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

巨大なバッチを分割して構成する 〜SQLバッチフレームワークBricolage〜

$
0
0

トレンド調査ラボの青木峰郎(id:mineroaoki)です。 好きなRubyのメソッドは10年前からString#slice(re, nth)ですが、 最近はRubyよりCoffeeScriptとSQLのほうが書く量が多くて悩んでいます。

今日はわたしが開発している「たべみる」の背後で働いている 巨大バッチの構成について話したいと思います。

たべみるのバッチは約3000行のSQLで構成されており、 処理時間が1日で4時間程度かかる、そこそこの規模のプログラムです。 このバッチ処理プログラムをBricolage(ブリコラージュ)というフレームワークで構造化する手法について説明します。

「たべみる」とは

まず最初に、「たべみる」がどういうものなのかごく簡単にお話ししておきましょう。

「たべみる」は企業のみに提供しているB2Bの分析サービスで、 クックパッドのレシピ検索の分析をすることができます。 具体的には、特定の語の検索頻度や、どんな語と一緒に検索されているか、 それから急激に検索が増えている語などを知ることができます。

例えば次は「バレンタイン」という語の検索頻度のグラフです。

▼「バレンタイン」の検索頻度グラフ(2014年〜2015年) f:id:mineroaoki:20150626162112p:plain

このグラフを見ると、「バレンタイン」という語の検索頻度は12月26日から上昇を始めていることが見てとれます。 つまりクリスマスが終わったとみるや次のイベント(バレンタイン)に向けて準備を始めているのですね。 恐ろしいことです。

「たべみる」バッチの構成

たべみるは上記の画面を500ミリ秒未満で表示することができます。 これは一般的なRailsアプリに比べれば遅いほうですが、 Amazon Redshiftに直接アクセスして6年分のデータ(10億件を余裕で越えます) に対して分析を行っていることを考えると、実は非常に高速と言える速度なのです。

このような分析を高速に実行できるようにするために、 背後では事前に集計を済ませた、いわゆる「サマリーテーブル」を大量に作成しています。 このサマリーテーブルを作るのが、たべみるバッチの主な仕事です。

たべみるバッチは日次(1日に1回の頻度)で動き、 次のような仕組みでサマリーテーブルを更新します。

▼たべみるバッチ概要フロー f:id:mineroaoki:20150626162328p:plain

まず元データをcookpad.comのメインデータベースであるMySQLと、 Treasure Data(Hadoop)から取得してRedshiftに入れます。 そのあとのキーワードの名寄せや集計はすべてRedshift上で行っています。

たべみるバッチではほとんどのデータ加工処理をSQLで記述しているので、 Redshiftの並列処理の恩恵を十分に受けることができます。

またSQLでの処理に更新(update)や削除(delete)はほぼ存在せず、 90%以上の処理は次のようなinsert selectで行います。 これは処理を羃等にし、vacuumを不要にするための工夫です。

insertinto keyword_combination_recipe_sets
select
    l.item_id as item_id
    , r.item_id as pairing_item_id
    , l.recipe_id
from
    keyword_recipe_sets l
    inner join keyword_recipe_sets r
    on
        l.recipe_id = r.recipe_id
        and l.item_id <> r.item_id
where:
            略
            :
;

巨大なバッチをジョブに分割する

たべみるバッチのSQLが3000行とは言っても、もちろん1つのSQLが3000行ではありません。 上記のようなinsert select文を使った20〜30行くらいのSQLをたくさん組み合わせることで バッチは構成されています。

さて、この大量のSQLをどうやって実行していくべきでしょうか。 とにかく実行するだけでいいのであれば簡単ですね。 例えばRubyなら次のようにpgライブラリを使えばSQLを実行できるので、 このコードでひたすら実行していけばいいわけです。

require'pg'

conn = PG::Connection.open(host: 'localhost', port: 5444, dbname: 'production', user: 'tabemirudev', password: '')
begin
  conn.query(<<-SQL)
      insert into base_search_counts      select…略…      ;   SQL

  conn.query(<<-SQL)
      insert into item_search_counts      select…略…      ;SQL

  conn.query(<<-SQL)
      insert into daily_si      select…略…      ;SQL

  conn.query(<<-SQL)
      insert into weekly_si      select…略…      ;SQL# 必要なだけたくさんSQLを書くensure
  conn.close
end

これではまずいのでしょうか?

当然、まずいのです。 まずいと言っても、メソッドに分けるべきとかそういうレベルの問題ではありません。 例えば次のような疑問・要望が湧いてきます。

  • 処理が失敗したとき、どうやって原因を追うのか?
  • 途中で失敗したらどうなるか? 失敗したところから再起動できるか?
  • SQLの単体テストはどうすればいいのか?

「運用しやすいバッチ」とは、「失敗したときに簡単に対処できるバッチ」のことです。 どこで、何が原因で失敗したのかすぐ特定でき、 問題を解決したら失敗したところから実行を再開できるのがよいバッチです。 しかもそのうえで開発しやすく、テストしやすい仕組みならベストでしょう。

このような要望を満たすために、 大きなバッチは小さな処理ごとに複数の「ジョブ」(プログラム)へ分割して、 バッチジョブの組み合わせで全体を構成するのが一般的です。 例えば1つのジョブは1つのSQL(insert select文など)を実行するだけのプログラムとし、 そのようなジョブをたくさん作ることで全体を組み立てるわけです。

SQLバッチフレームワークBricolageによるジョブ化

このようなバッチジョブを作るために、 たべみるでは独自開発したBricolageというフレームワークを使っています。 「SQL」バッチフレームワークとは言っていますが、現在は主にRedshiftを想定した実装になっています。 Bricolageは先日GitHubでOSSとして公開しました(下記URL)ので、誰でも自由に使うことができます。

Bricolageを使ってジョブを作ると決まった場所へ自動的にログが取られますし、 後述するような様々な機能を活用することで容易に開発や運用ができるようになっています。

たべみるの日次バッチは現在200程度のBricolageジョブで構成されています。 その200くらいのジョブの依存関係を図で表すと次のようになります。

▼ジョブフロー図 f:id:mineroaoki:20150626152818p:plain

Bricolageは1つ1つのジョブを作るためのフレームワークなので、 複数のジョブを連動させて上記のようなジョブフローとして動かす仕組みはあまり作り込んでいません。 いまのところ、ジョブフローをCUIで非並列で動作させる簡単な仕組み(bricolage-jobnetコマンド)だけを用意しています。

ジョブフローを実行する仕組みはジョブ管理システムに任せるのがベストでしょう。 代表的なジョブ管理システムとしては、 日立のJP1/AJS3NRIの千手IBMのTivoli Workload Schedulerなどが挙げられます。

またOSSのジョブ管理システムとしては、 Hinemos(NTTデータ)、 RundeckAzkaban(LinkedIn)、 Airflow(AirBnB)などがあります。

機能だけで言えば個人的にはJP1/AJS3が使いたいのですが、ちょっと高いのと、運用が大変なので導入していません。 しかし最近、バッチの時間がのびてジョブフロー全体の最適化が緊急の課題になりつつあるので、 そろそろ何かしらのジョブ管理システムを導入する予定です。 ジョブフローのGUIは絶対にほしいので、まずはHinemosから試そうと思っています。

Bricolageの特長

Bricolageには次のような特長があります。

  1. SQLにパラメーターを埋め込める
  2. SQLの定型パターンを自動生成できる
  3. dry-runとexplainが可能

利点1. SQLにパラメーターを埋め込める

Bricolageでは「$変数名」のような記法でSQLに任意のテキストを埋め込めるようにしています。 例えば最初にinsert selectの例として見せたSQLは実は変数の展開後のコードで、 ソースファイルには次のように書かれています。

insertinto $dest_table
select
    l.item_id as item_id
    , r.item_id as pairing_item_id
    , l.recipe_id
from
    $keyword_recipe_sets l
    inner join $keyword_recipe_sets r
    on
        l.recipe_id = r.recipe_id
        and l.item_id <> r.item_id
where:
            略
            :
;

あえてprepared statementを無視して純粋に文字列ベースでパラメーター展開(置換)をしているので、 式やテーブル名、カラム名もパラメーターにすることができます。 上記のSQLでもテーブル名を変数にしています($dest_tableと$keyword_recipe_sets)。

また、バッチ全体、サブシステム、ジョブのそれぞれのスコープで変数を定義でき、 実行時に上書きすることも可能です。 そのため、本番で一時的にターゲットテーブル名を差し替えてコピーを作る、など柔軟に運用することができます。

さらに、eRubyのタグを使ってRubyの式を埋め込むこともできます。 例えば本番用のサマリーテーブルを複数まとめて切り替えるためのジョブでは次のようなコードを使っています。

begin transaction;
<% publish_tables.each do |table| %>
  altertable $schema.<%= table %> renameto<%= table %>_org;
  altertable $schema.<%= table %>${target_suffix} renameto<%= table %>;
<% end %>
commit;

この切り替えの処理対象となるテーブルは20以上あり、しかも頻繁に増減するので、 手でテーブルリストをメンテナンスするのは事故のもとです。 この方式ならばそのような変化するテーブル群をまとめて扱うことができます。

ちなみに対象となるテーブルは次のようにテーブル定義にBricolage独特の記法で 「publish」属性を付けることで宣言できるようになっています。 「--attributes」で始まっている行が属性の宣言です。

--dest-table: ANYly_stats--attributes: [publish, replicate]createtable $dest_table
( data_date date encode delta
, item_id int encode lzo

, si real encode raw
, sv real encode raw
, ysi real encode raw

, ci real encode raw
, csv real encode raw
, yci real encode raw
)
distkey (item_id)
sortkey (data_date)
;

利点2. SQLの定型パターンを自動生成できる

2つめの利点は、よく使うSQLの定型パターンをオプションだけで自動生成できることです。

例えばテーブルを洗い替えする場合、ターゲットテーブルと同じ定義のテーブルを別途作成し、 insert selectを完了してからrename(alter table)ですりかえるという手法をわたしは好んで使います。

そのような場合、メイン処理となるinsert select文の他に、 テンポラリーテーブルのdrop table文、create table文や、renameのためのalter table文なども必要になります。 またRedshiftの場合は統計をとるanalyze文やソート順を改善するvacuum sort only、 それに権限を与えるgrantも合わせて実行したいところです。 つまり全体では次のようなSQLになるわけですね。

droptable $dest_table;

createtable $dest_table
( ……
, ……
);

insertinto $dest_table
select……
;

vacuum sort only $dest_table;

analyze $dest_table;

grantselecton $dest_table to $tabemiru_reader_group;

たべみるの場合、余裕で100以上のテーブルがあるので、 これらについていちいちdrop、create、rename……と書きたくはありません。 そこでBricolageでは「ジョブクラス」という仕組みを使ってこれらを生成します。

ジョブクラスは、ジョブで実行されるSQL文のテンプレートです。 例えば「rebuild-rename」というジョブクラスを指定すると、 さきほど述べたdrop、create、renameはBricolageが生成してくれます。 開発者が書かなければいけないのはメインのinsert select文だけです。

また、ほとんどのジョブクラスにはanalyzeオプションやvacuum-sortオプションが 設定されており、これをtrueにするだけで処理後にanalyze、vacuum sort onlyが実行されます。

ジョブクラスとそのオプションはジョブごとに「xxx.job」という名前のYAMLファイルで 指定するようになっています。例えば次のような感じです。

class: rebuild-drop
dest-table: $app_schema.weekly_stats${target_suffix}
src-tables:weekly_si: $work_schema.weekly_si
    yearly_si: $work_schema.yearly_si
    latest_yearly_si: $work_schema.latest_yearly_si
    moving_si: $work_schema.moving_si

    weekly_ci: $work_schema.weekly_ci
    yearly_ci: $work_schema.yearly_ci
    latest_yearly_ci: $work_schema.latest_yearly_ci
    moving_ci: $work_schema.moving_ci
table-def: weekly_stats.ct
vacuum-sort:trueanalyze:truegrant:privilege: select
    to:"$tabemiru_reader"

もちろんこの他に、MySQLからRedshiftへのテーブルコピーのように利用頻度が高いパターンも 「ジョブクラス」として用意してあります。

利点3. dry-runとexplainが可能

Bricolageで作成したジョブはbricolageコマンドで実行できますが、 そのとき、次のように--dry-runオプションを付けることで、SQLを実行せずに表示だけさせることができます。 パラメーターとeRubyタグも展開されて表示されます。

% bricolage --dry-run--job recipe_sets/keyword_combination_recipe_sets-rebuild.sql.job
                                                                            -- 最初のほうはオプションで自動生成されている
\timing on

\set ON_ERROR_STOP false
drop table tabemirudev.keyword_combination_recipe_sets cascade;
\set ON_ERROR_STOP true

-- recipe_sets/keyword_combination_recipe_sets.ct          ここはテーブル定義ファイルから読み込まれた
create table tabemirudev.keyword_combination_recipe_sets
( item_id int encode delta, pairing_item_id int encode delta, recipe_id int encode lzo)
distkey (recipe_id)
sortkey (item_id)
;

-- recipe_sets/keyword_combination_recipe_sets-rebuild.sql.job     以下がSQLファイルに書いた部分
insert into tabemirudev.keyword_combination_recipe_sets
select
    l.item_id as item_id
    , r.item_id as pairing_item_id
    , l.recipe_id
from
    tabemirudev.keyword_recipe_sets l
    inner join tabemirudev.keyword_recipe_sets r
    on
        l.recipe_id = r.recipe_id
        and l.item_id <> r.item_id
where
        以下略

バッチでdry-runができることがどれだけありがたいかは、 バッチを運用したことのある人ならよくわかるでしょう。 実行されるSQLが事前にわかるのは精神衛生上たいへんいい効果があります。

また、--explainオプションを付けて実行すると、 メインとなるinsert selectの部分だけexplainを付けて実行することもできます。 こちらは実際にSQLがサーバーに流れるので、 文法と意味解析チェック(型チェックとか)の代わりとしても使うことが可能です。

Bricolageのその他の機能

ここで説明したBricolageの機能は一部にすぎません。 この他にBricolageには複数データソースを管理して切り替えられる機能や、 ストリーミングロードの仕組みなどが用意されています。

本番投入優先で実装しているのでドキュメントがだいぶ雑なのですが、 気になったかたはぜひGitHubのWikiを眺めてみてください。

Bricolageの今後の開発予定

今後、Bricolageでは次の2つの機能の導入を予定しています。

  1. ジョブ管理システム
  2. ジョブ全体のユニットテスト機能

ジョブ管理システムはHinemosから試すつもり、と書きましたが、 それはそれとして選ぶのも面倒になってきたので、自分で書いてしまおうかなとも思っています。 ジョブ管理システムはいろいろ面倒なことが多いので自分で書くのは避けてきたのですが、 いつまでたっても手頃なものが出てこないので痺れを切らしました。

それから、他にぜひ導入したいのがジョブのユニットテスト機能です。 入力データと正解データを書いておいたら自動的に検証してくれるような機能を予定しています。

『10年戦えるデータ分析入門』6月30日発売!

最後に個人的な宣伝です。

10年戦えるデータ分析入門 SQLを武器にデータ活用時代を生き抜く (Informatics &IDEA)

10年戦えるデータ分析入門 SQLを武器にデータ活用時代を生き抜く (Informatics &IDEA)

わたしの、たぶん10冊目の著書 『10年戦えるデータ分析入門 SQLを武器にデータ活用時代を生き抜く』 が、来週6月30日に発売されます。 池袋ジュンク堂など一部の書店ではすでに先行販売を実施中です。

本の発売タイミングがあまりにもブログ当番のタイミングと合いすぎていて 作為的なものすら感じますが、なんとまったくの偶然です。 わたし自身もびっくりしました。

RedshiftやHadoop(Hive)、Presto、Sparkのような並列分析データベースがメジャーになるにつれ、 SQLによるデータ分析は適用範囲を増しています。 これらのシステムの適用範囲は現在のところその場で考えながらクエリーを投げる探索型の分析がメインですが、 その次には分析クエリーをバッチとして定型化・固定化してダッシュボードで監視したり、 システム連携するパターンも増えていくでしょう。 今回紹介した仕組みはそのような場面で役立つはずです。 ぜひ手元のツールボックスの1つとして分析バッチの仕組みを加えておいてください。


検索ログから「じわじわ検索頻度が上昇しているキーワード」を見つける

$
0
0

こんにちは。トレンド調査ラボの井上寛之(@inohiro)です。

普段は法人向けサービス「たべみる」の開発を担当しています。 たべみるはクックパッドの検索ログを基にしたサービスで、任意のキーワードの検索頻度、キーワード同士の組み合わせ検索頻度、 およびそれらを地域や年代・性別で絞り込んで分析することができます。

トレンド調査ラボでは「たべみる」の開発のほか、 クックパッド上のトレンドを見つけるために日々調査を行っています。 ここでのトレンドとは、「流行っている」もしくは「流行りそう」といったものを指します。 消費者が気になっているキーワードが何かを知ることで、消費者が求めている情報を適切に提供できると考えています。

今回は、膨大な検索ログの中から「じわじわ検索頻度が上昇しているキーワード」を見つけるために 行ったことについて紹介したいと思います。

じわじわ検出

「じわじわ検索頻度が上昇している流行りそうなキーワードを、機械的に、いち早く見つける」ことを 「じわじわ検出」と呼ぶことにします(「流行り」については後述します)。

例えば最近流行したキーワードとしては「塩レモン」や「おからパウダー」などがあります。 下図は、「おからパウダー」「ガパオライス」「塩レモン」の、2009年3月から2015年2月までの週間検索頻度の推移です。

f:id:InoHiro:20150629154133p:plain

このようなキーワードを、青い矢印で指すような流行りはじめのうちに見つけられると嬉しいわけです。

そもそも「流行っている」とは?

そもそも「流行っている」とはどういうことなのでしょうか。 じわじわ検出で検出対象となるようなキーワードの検索頻度推移から、共通する点を見つけ出し、 式のように定義できると検出ができそうです。つまり、「流行っている」の定義がこの分析の肝と言えます。 試行錯誤を繰り返し、現在は「流行っている、流行りつつある」を以下のように定義しています。

  • 「1年前の検索頻度を上回っている週数 / 比較対象の週数(N)>= M」であるとき、流行っている、流行りつつある
    • N:比較対象の週数(52週、40週、27週など)
    • M:検索頻度が1年前の同一週よりも上回っている週のNに対する割合(70%、80%、90%など)

文章として書いてみると、「N週の間で、週の検索頻度が一年前の同一週の検索頻度と比べて上回っている週の割合が、M%を超えている」とき、 そのキーワードが流行っている、流行りつつあるということです。

例えば以下の図は、N=10、M=90%で「2015年流行っている」と判定されるような例です。 10週(N)のうち、2015年の90%の週が2014年を上回っており、M >= 90% の条件を満たしています。

f:id:InoHiro:20150629154155p:plain

一方以下の図は、「流行っている」と判断されない例です。 2014年を上回る週は60%であるため、M >= 90% の条件を満たしていません。

f:id:InoHiro:20150629154214p:plain

今回はNに57から27のような大きめの値を設定しましたが、このように設定した理由には次に挙げるような背景があります。

  • 季節要因を排除したい

    • 例えばクリスマスやバレンタインデー、お正月などの季節要因による影響を抑えたい
  • バースト(一過的な爆発的な数値の変化)を排除したい

    • 主にテレビで取り上げられたことによるバーストを排除したい

Nが小さいと、調査対象の期間によっては季節的な要因によって「流行っている」ように見えてしまう可能性があります。 同様に、短い期間のうちにバーストした場合、非常に大きな影響を受ける可能性があります。 そのため、Nは大きめの期間(52~27)を使うのが良いと考えました。

下図は「ガパオライス」の検索頻度のうち、2014年第2-26週と2015年2-26週を比べたグラフです。 ほぼ全ての週で、2015年の検索頻度が2014年の検索頻度を上回っているのが分かります。 絶対値はどうであれ、検索されている(≒多くの消費者が注目している)キーワードであると言えます。 また、ある程度の期間で比較しないと、季節や、テレビなどによるバーストの影響を受けてしまう可能性がある ことが分かると思います。

f:id:InoHiro:20150629154234p:plain

一方、季節要因やバーストの影響を受けない代償として、新語(これまで存在しなかったワード)を見落とす可能性があると言えます。 新語の場合、前52週のうちの多くが検索頻度0である可能性が高く、急に検索され始めたワードは今回の定義では見落とす可能性が高いです。 ただし新語の発見は「じわじわ検出」の目的では無いので無視します。

SQLによる分析

SQLを使って、時系列データから流行っている、流行りつつあるキーワードを探してみました。 SQLを使う理由は、分析対象となるキーワードごとの週間検索頻度が、 Amazon Redshiftに蓄積されているためです。 分析の流れは以下のようにしました。

  1. 年ごとに週番号を付ける
  2. 1年前の同一週とくらべて検索頻度が上回っている週を見つけ出し、その割合を計算する
  3. 対象週数Nと、上回っている収集の割合Mを指定して、最終的なキーワード集合を得る

前提

前提として、分析対象のデータは以下の様なスキーマのテーブルに入っているとします。

createtable weekly_si
( data_date date-- 週の開始日
  , keyword_id int -- キーワードID
  , si float-- 検索頻度
)
;

1. 年ごとに週番号を付ける

row_number()(ウインドウ関数)で、年ごとの週番号をつけたリレーションを作っておきます。 なおwith句を使えば、いちいち表を作らずに2と一緒に実行することができます。

createtable numbered_weekly_si
( week_num int     -- 週番号
  , data_date date-- 週の開始日
  , keyword_id int -- キーワードID
  , si float-- 検索頻度
)
;

insertinto numbered_weekly_si
select
    row_number() over(partition by keyword_id, extract(year from data_date) orderby data_date) as week_num
    , data_date
    , keyword_id
    , si
from
    weekly_si
;

2. 1年前の同一週とくらべて、上回っている週にフラグを立てつつ、N週のうちの割り合いを計算する

1で作った週番号付きの検索頻度を、年をずらしてジョインし、1年前の同一週と比較を行います。 1年前を上回っている場合フラグ(si_growth)を 1に、そうでなければ 0としておきます。 この結果に対して、ウインドウ関数を使って比較対象の週数Nにおける、上回っている週数の割合を求めます。 以下のクエリでは、52週だけでなく、40週、27週についても集計しています。

createtable weekly_si_growth
(
  week_num int             -- 週番号
  , current_data_date date-- 週の開始日
  , si_growth int          -- 検索頻度が成長しているか (0,1)
  , keyword_id int         -- キーワードID
  , ratio52 float-- 前52週での検索頻度成長割合
  , ratio40 float-- 前40週での検索頻度成長割合
  , ratio27 float-- 前27週での検索頻度成長割合
)
;

insertinto weekly_si_growth
select
    *
from (
    select
        week_num
        , current_data_date
        , si_growth
        , keyword_id
        -- 週数N {52, 40, 27} 毎に、1年前同一週の検索頻度を上回っている週の割合を計算-- 分母は定数を直接書くこともできるが、必ずしも {52, 40, 27} 週前まであるとは限らないので、-- 同様に数え上げる
        , sum(si_growth) over(partition by keyword_id orderby current_data_date rows52 preceding) * 1.0
                / sum(1) over(partition by keyword_id orderby current_data_date rows52 preceding)
              as ratio52
        , sum(si_growth) over(partition by keyword_id orderby current_data_date rows40 preceding) * 1.0
                / sum(1) over(partition by keyword_id orderby current_data_date rows40 preceding)
                as ratio40
        , sum(si_growth) over(partition by1orderby current_data_date rows27 preceding) * 1.0
                / sum(1) over(partition by1orderby current_data_date rows27 preceding)
                as ratio27
    from (
        selectcurrent.week_num
            , current.data_date as current_data_date
            -- 1年前の検索頻度を上回っていたら 1 とする
            , case when (current.si - last_year.si) > 0then1else0endas si_growth
            , current.keyword_id
        from
            numbered_weekly_si current-- 1年前の同一週とジョイン
            inner join numbered_weekly_si last_year
            oncurrent.week_num = last_year.week_num
                andcurrent.keyword_id = last_year.keyword_id
                and extract(year fromcurrent.data_date) = extract(year from last_year.data_date) + 1
    )
    groupby1, 2, 3, 4
)
;

3. パラメータ(対象週数N、週数の割合M)を指定して、条件を満たすワードを得る(検出)

2で、全てのキーワードについて、週数N(52週、40週、27週)での検索頻度が上昇している割合を計算したので、 期間と検出に用いる週数N、閾値となる割合Mを指定して、その条件を満たすワードを出力します。 ここでは下記のパラメータで問合せるクエリを示します。

  • 期間:2014-01-01 から 2014-12-31
  • N: 52週
  • M: 90%
    • つまり、2014-01-01から2014-12-31のそれぞれの週から52週前までの期間において、1年前の同一週の検索頻度を上回っている週の割合が 90%以上であるようなキーワード

appeared_sumには期間内(上の条件の場合52週)のうち、 前の52週(N)の中で1年前の同一週の検索頻度を上回っている週が90%である週の数が入ります。 つまり、appeared_sumは期間中の出現頻度で、多ければ多いほど、 流行っている、流行りつつあるキーワードと言えます。

select
    candidate.*
    , keyword.name
    , appeared_sum
from (
    select
        keyword_id
        , sum(appeared) as appeared_sum
    from (
        select
            keyword_id
            , count(*) as appeared
        from
            weekly_si_growth
        where
            ratio52 >= 0.9-- 前52週において、上回っている週が 90% 以上and current_data_date betweendate'2014-01-01'anddate'2014-12-31'groupby
            keyword_id
    )
    groupby
        keyword_id
) candidate

-- キーワード名を得るためのジョイン
inner join keywords keyword on candidate.keyword_id = keyword.id
;

比較する週数(N)を調整することで、季節やテレビなどの影響を平均化したり、その逆を行うことができます。 今回は52週のときのみ取り扱いますが、40週、27週の値を使うと、より敏感にトレンドを掴むことができます(言うまでもなく、その分ノイズも増えます)。

検証

見つけ出したかったキーワードを、上記の方法で本当に見つけられるのか検証を行いました。

検証方法

2014年および2015年上旬に流行った、流行りつつあったキーワードのうち、 少なくともこれだけは見つけ出したかったというキーワードを正解ワードセットとし、それらを見つけ出せるか調べました。 正解ワードセット(25ワード)は以下になります。

豚ロース薄切り, おからパウダー, サンドイッチ, ジャージャー麺, ジャーマンポテト,
トースト, ミネストローネ, ラタトゥイユ, ポップオーバー, マッシュポテト,
スクランブルエッグ, 水菜サラダ, バタービール, フラックスオイル, 白湯(パイタン)鍋,
マシュマロ, 安納芋, ガパオライス, なす煮びたし, 温泉卵の作り方,
塩レモン, マッサマンカレー, 亜麻仁油, 台湾まぜそば, 寝かせ玄米

また、前年同一週を上回っている週数の割合(M)は、小さくすればそれだけ検出する語が増えるので、 90% → 80% → 70%と変化させてみました。

検証結果

前年同一週を上回っている週数の割合が90%のとき

  • 検出した語: 3133 ワード
  • 正解数: 15 ワード(以下)
豚ロース薄切り, おからパウダー, ジャーマンポテト, ミネストローネ, ラタトゥイユ,
マッシュポテト, スクランブルエッグ, 水菜サラダ, マシュマロ, 安納芋,
ガパオライス, なす煮びたし, 温泉卵の作り方, 塩レモン

前年同一週を上回っている週数の割合が80%のとき

  • 検出した語: 5988 ワード
  • 正解数: 21 ワード(90%の時に見つけたワードと、以下)
サンドイッチ, ポップオーバー, バタービール, マッサマンカレー, 台湾まぜそば, 寝かせ玄米

前年同一週を上回っている週数の割合が70%のとき

  • 検出した語: 10089 キーワード
  • 正解数: 22 ワード(80%の時に見つけたワードと、以下)
ジャージャー麺

結果について

適合率(Precision)を上げすぎて今後流行る可能性のあるキーワードを落としてしまうよりも、 検出される語が多くなってしまっても、正解ワードが多く含まれる結果(高い再現率(Recall))を目指しました。 結果として再現率は、M=90%で0.6、M=70%で0.88となりました。

正解データのうち、発見できなかったキーワード

正解キーワードのうち、発見できなかったキーワードはなぜ発見できなかったのでしょうか。 それぞれのキーワードについて調べてみると、以下の様な理由であることがわかりました。

  • トースト、フラックスオイル

    • 検索頻度が1年前同一週とくらべて増加していない
  • 白湯(パイタン)鍋

    • 検索頻度が上昇し始めたのは2014年11月ごろから
    • 52週のうち90%で検索頻度が上昇しているとは言えない

そもそも「トースト」「フラックスオイル」の2ワードについては、 組み合わせて検索される頻度が上昇したことによって 「見つけ出したかったキーワード」(正解ワード)とされていたため、 今回の方法では見つけ出すことができませんでした。

2015年流行りそうなキーワードは?

この方法を2015年上旬の検索ログに適用し、流行語の候補を探してみました。 期間を2015年1月1日から6月20日までとして、N=52週、M=90%として実行したところ、1490ワードを得ました。 その中から、特にこれは流行るかも!?というような候補をいくつか挙げておきます。 ウェブやテレビ、お店などで見つけたら、このエントリのことを思い出して下さい。

その他試したアイデア

実際に今回試したアイデアに至るまでは、多くの試行錯誤を繰り返しました。 試したアイデアをいくつかご紹介しようと思います。

  • あるキーワードの検索頻度が、初めて閾値Nを超えたなら、そのキーワードは流行りはじめているのではないか?
  • 前の週の検索頻度とくらべた差が一定期間、M%以上の上昇を続けていたら、そのキーワードは流行っているのではないか?
  • 検索頻度の移動平均がL%ずつ上昇を続けていたら、そのキーワードは流行っているのではないか?
  • 組み合わせて検索されているキーワードの数が爆発的に増えているキーワードは、流行っているのではないか?
    • 実際には「組み合わせた検索が増える」というのは、探しているキーワードの認知度が高くある必要があるので、見つけることができても既に流行ってしまったキーワードであった(目的である流行りはじめは検知できない)
  • 検索頻度の変化率X%以上の立ち上がり検知し、その後も数週間にわたって検索頻度を維持し続けているとき、流行った状態にあるのでは?
    • テレビ等によるバーストを、流行ったと誤検知したくない
    • こちらも流行ったことはわかるが、流行る前に見つけ出したい

まとめ

本稿では、検索ログから「じわじわ検索頻度が上昇しているキーワード」を機械的にいち早く見つけ出すために 検討し、実際に行った方法、および得られた結果についてまとめました。

今後も定期的に「じわじわ検索頻度が上昇しているキーワード」を見つけるため、方法の改善を行っていこうと考えています。 また、人間の手による温かみのあるパラメータ調整ではなく、機械学習などを用いてより低コストに導き出せないか、 挑戦していこうと考えています。

Cookpad × CyberAgent × DeNA の15卒エンジニア交流会を開催しました

$
0
0

こんにちは、新規広告開発部エンジニアの @cnosukeです。 少し前の話になってしまいますが、8月14日(金)に CyberAgent さん、 DeNA さん、そして弊社の15卒エンジニアの交流会をCookpad キッチン&ラウンジで開催しました。

f:id:cnosuke:20150826155844j:plain

3社の15卒エンジニアに絞ったのはなぜか

Web領域では毎日のようにエンジニアのイベントが開催されていますが、今年入社したばかりのエンジニアはあまり参加したことが無い人も多く、また、何度か参加したけど常にボッチで行くモチベーションが無くなってしまったという人もいました。そこで、年齢が近く心理的障壁が少ない同年代の友人を先ずは増やすことで、他のイベントに行った時に知っているひとがいる状態になって参加しやすい、また、興味があるイベントにお互いに誘って参加するということも出来るようになるかなと思い、同年代のWebエンジニアでまずは交流会をしようという話になり、開催しました。

なぜこの3社のみのエンジニアで開催したのか、疑問に思う方もいらっしゃるでしょう。(特に他社の15卒エンジニア数名から行きたかったという声を頂戴しました 🙇🙇🙇 ) 理由はシンプルで、「初回だから」です。開催したメンバーとしては、このような交流の会は継続的に行いたいという思いがあります。だからこそ、初回は小さく始めることにしました。 今後の継続的な友人関係に繋がるような交流会にしたい、ちゃんと参加者の顔と名前が会の後でも記憶に残っているような会にしたい、そのためにはどのように会を設計したらいいのか、全く分かりませんでした。とりあえず、少ない人数で開催すれば、より継続的な友人関係のための交流に近づけるかなと思い、初回は少ない人数で開催しました。

ちなみに、次回は3社に限定せずに開催しようと考えています。参加人数や募集方法、コンテンツや開催場所などなどは何もまだ考えていませんが、目的としては変わらず、継続的な友人関係に繋がる交流会という感じに考えていて、それに基づいてコンテンツや人数等を考えるつもりです。

コンテンツは「会話」

さて、3社の15卒エンジニアの交流というわけですが、ずっと座って話を聞くというのでは長期的な交流に繋がらないと思ったので、とにかくお酒を飲みながら(花金ですし!)会話をしてもらうようにしました。最初に各社から参加者全員の紹介をざっくり行い、あとはLTを10本くらいゲリラ的に行いました。

f:id:cnosuke:20150814210311j:plainf:id:cnosuke:20150814210339j:plain

次回開催とその後

まだ何も決まっていませんが、継続的な取り組みとして今後も今回の参加者同士で交流会を開催していきたいと考えています。次は3社の枠を外したいとは思いますが、ギリギリ名前と顔知ってるかな程度という関係にはならないように、コンテンツや参加人数等の会の枠組みはしっかり考えるつもりです。

例えば会社は違っても、ふらっと一緒にランチに行ったりふらっと飲みに行ったり、休日にサイクリングやボルタリングに一緒に行くとか、そんな関係になれるような交流を作っていきたいと考えています。くだらない雑談や趣味のこと、当然技術的な質問や相談もお互いにしあって、互いに刺激を与えながらよりよいサービスをみんなが作れるようになると世界は少し幸せになれるかなとか、思っています。

いまさら聞けない「コードの英語」超入門

$
0
0

広告事業部の鈴木達矢です。コーディングをしてると変数やメソッド名の付け方に悩むことって多々ありますよね。逆にコードを読んでいると単語の選択がこれでいいのかなという時や、動詞の活用形が間違っていてよく意味がわからない、時に潔く日本語の変数名になっていることも見かけます。でもプログラミング言語の単語が英語をベースにしていますし、Railsを使っている場合は日本語が規約(Convention)に合わなかったりします(複数形が無いなど)。それから動詞の活用形が違っていると主語(動作の主体)が変わってしまい、意味が変わってしまいます。その結果コードの可読性が落ち混乱を招きやすくなります。

いくつかの基本的な法則だけおさえておけばコーディング中に可読性の高い単語の選択ができるようになります。今回はそれを目的に、英語の扱いに都度時間を費やしてしまうような方に向けていくつかの法則をご紹介します。*1

変数やメソッド名の選定

単語の意味の範囲

辞書を引いて見つかった単語をそのまま使っているケースをよく見かけます。日本語の単語の意味の範囲と辞書で見つかった英単語の意味の範囲が異なる場合があるので注意が必要です。

例. 広告のキャンペーン企画の案件終了日時の終了日に使う変数として以下の名前をつける例

  • 日本語:案件終了日時
  • 変数名:issue_end_datetime

案件終了日時という単語は一つの単語としては英語辞書にはありません。ですので、それぞれの単語をばらばらに辞書で引いた結果、案件_終了_日時という変数名が出来上がったのではないかと思われるケースです。正解は無いですが私の場合はcampaign_ends_atと命名すると思います。*2

ここで浮かび上がるのが案件に対してissueという単語が適当なのかという疑問です。辞書を引く際に前提として注意すべきなのが、「日本語と英語では単語の意味の範囲が違う」ということです。

f:id:szkmp:20150831141900p:plain

上の図は案件issueをそれぞれ類語辞典で引いた結果です。争点、出来事、問題、論点といった意味では意味の範囲が重なっています。そのためissueという単語が選ばれたのですが、今回のキャンペーン企画プロジェクトという意味では意味の範囲が重なっていません。issueという単語をその意味の案件として使うのには無理がありそうです。

ある単語の意味の範囲をどこに据えるか、言い換えるとどのシーンを言い表すのに適当な単語を使用するのかというのはまさに集合知で、それが決まるまで判断には文化的背景が深く関わります。それから日本語には曖昧な単語を広い場面で使用する癖があるというのも知っておくと良いと思います。*3

つまり、そういったことを前提により正確に表したいものをまずは再定義するとより良い訳に近づけると思います。さらに英語の類語辞典を使ってより意味が重なる範囲が多い単語を選ぶと良い結果に近づけます。この場合は企画計画という意味に近いcampaignという単語を選びました。

日本語変数名

日本語変数名はそれが表すものが固有名詞の場合は使っても良いと思います。例えばクックパッドにはあるレシピに対してつくったよをレポートするつくれぽという機能がありますが、これは説明すると長い概念ですからひとまとめの固有名詞としてtsukurepoと呼ぶのは良いと思います。

それ以外の場合は、先にもあげたようにRailsの場合規約に合わなかったり、アルファベット表記の日本語がいっぱいあると可読性が落ちたりするので基本的には英語に寄せるのが良いでしょう。

原形・現在分詞(-ing)・過去分詞(-ed)?

時々見かけるのが動詞の活用形が間違っているケースです。間違えると意味が変わってしまうので注意が必要です。以下に最も推薦できる形をリストしました。

コミットコメント

コミットタイトル(最初の行)は基本的に動詞の原形で始めます。本文は普通の文章で説明的に書いて大丈夫です。

Summarize changes in around 50 characters or less

More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.

引用元

コード中のコメント

対象のメソッドやラインを主語として動詞の原形、もしくは三人称単数形で書かれることが多いようです。プロジェクト中でそのどちらかに統一されていれば良いと思います。

# Converts the object into textual markup given a specific format.## @param format [Symbol] the format type, `:text` or `:html`# @return [String] the object converted into the expected format.defto_format(format = :html)
  # format the objectend

引用元

メソッド名

各言語によって慣習が異なると思いますが基本的にはメソッドが所属するクラスを主語とした時の動詞の活用形に合わせます。

classCar# A car runs# 主語: Cardefrunend# A car is running?# 主語: Cardefrunning?end# A car has navigation system?# 主語: Cardefhas_navigation_system?end# A car is permitted to be shipped?# 主語: Cardefshipping_permitted?end# Someone commands to ship a car to a country.# 主語: 誰か(業務ロジック中の主体)defship_to(country)
  endend
  • has_navigation_system?

Railsの慣習では疑問系に三人称単数を使います。自然言語としての英語の疑問系はDoes the car have navigation system?もしくはThe car has navigation system?ですが、おそらく変数から呼び出すときのcar.has_navigation_system?がより自然に響くので後者の疑問形の例の形になっているのだと思います。

  • shipping_permitted?

自然言語ではA car is permitted to be shipped?が自然に響くのでpermitted_to_be_shippedでも良いですが、長いのでpermitとshipを一つの形容詞的な感覚にしてshipping_permitted?としています。(この辺は賛否あると思います。)

  • ship_to(country)

蛇足ですが、最後のship_to(country)はshipが他動詞なので(shipは自動詞にもなりうりますが、この場合Carが何かを出荷しするわけではない)ほかに主語がありそうに感じます。だから、(実際にはCar classにあるような場合もよく見かけますが)主語がCarと異なるのでなんとなく別の場所に実装があるべき匂いがします。

RSpecの英語

RSpecの英語は特殊です。英語の短縮形に似ていますが、もっと独特な書き方をします。よく気になるところを挙げてみました。*4

whenの後の過去分詞

describe Car
  describe "#"do
    context "when it is permitted to be shipped"doend# 主語とbe動詞を省略して良い
    context "when permitted to be shipped"doendend
end

describeされている主語(Car)を形容する感覚でpermittedを使うことができます。permitするのはCarではなく、Carpermitされるので過去分詞です。

このような感覚で主語を省略し、よりcontextを短くすることができます。長い小説的な文章によるcontextは可読性が落ちますのでなるべくこの形をとるのが望ましいです。

前置詞の後は現在分詞

そんなの当たり前だと思われる方もいるでしょうが前置詞の後に動詞の原形があるのをよく見かけます。forやaboutなどの前置詞のあとは名詞的に現在分詞を使用します。 it "consumes a litre of petrol for completing a round trip"*5

日本語の癖

動詞を名詞的に使う

日本語はすごく便利で熟語的に単語をつなげることで名詞を形容的に使い事物を表すことができます。しかし、同じことを英語で行うと別の意味になることがしばしばあります。例えばよく見かける例としては単語をなんでも名詞的につなげて変数名とするケースです。

classCardeffuel
    complete_round_trip = true/falseを返す処理
    ...
    if complete_round_trip
      ...
  endend

このような一時変数名を見かけた場合はまっさきに往復行程を完了せよという意味のメソッドだと思ってしまいます。completeが動詞として脳に飛び込んでくるからです。おそらく最初に熟語的に日本語でコンプもしくはコンプリートするのコンプリートを名詞的に羅列し、その結果を直訳した結果ではないでしょうか。しかし、英語のシンタックスが分詞と語順に支配されていますので原形の動詞が先頭に来るとまずは命令形である往復行程を完了せよという意味を思い浮かべてしまいます。Carround tripを完了したという形容的な意味で過去分詞を使用してround_trip_completedとした方が良いです。

日本語はコンテキスト依存の高い省略の多い言語

これは日本語に限ったことではないのですが、日本語はよりコンテキストへの依存が高く正確でならないといけない場面で思わぬ間違いを引き起こすことがあるので注意が必要です。

  • 現にあったopenという名前による勘違い

これは実際に最近あったケースです。かなり過去にコンテスト開催中という意味でContest.openというメソッドが定義されていて、ページビューの集計にそのメソッドを使おうとしたところ集計期間が足りなさそうということが発覚したということがありました。実際にはコンテストには公開中に募集期間審査期間発表期間という三つの期間の概念がありましたが、openがカバーしていたのは募集期間のみで、それに対して集計処理は全ての期間を集計対象としていました。

募集期間に対してopenという単語が選択されていたわけですががopenだったのか翻訳の過程で失われてしまっていました。募集がopenだったのか案件の公開状態がopenとも取れるので、さらに具体的に意味を狭めるopen_entryなどをここでは名前として使用すべきでしょう。コンテスト開催中とはユーザさんにとっては募集期間中であり、公開期間中を通じた訪問者数を知りたいキャンペーン主の立場からすると先述の3つ全ての期間なのでした。このように日本語はコンテキストによって意味が変わりやすいので単語の選定の際には注意が必要です。

便利なサイト

意味をキーに類語がグループ化されているので自分が欲しい類語を探しやすいです。

類語(synonyms)だけでなく上位語(hypernyms、より広義の単語)、同じコンテキストで登場する単語(same context)、今調べている単語を意味の解説に持つ語(reverse dictionary)など様々なアルゴリズムの関連語を提示してくれます

Stackexchangeの英語サイトでword-choiceのタグが付いている質問の回答がよく解説してあったりして便利です。時間があってとことん突き詰めたいときはこちらで質問すると便利です。

まとめ

  • それぞれの言語にそれぞれの単語の範囲があることを意識してより狭義の意味で短い単語を選定しよう
  • 主語がだれなのかを意識して動詞の活用形を決めよう
  • 日本語の癖(ハイコンテキスト、名詞を形容詞的に使う)が単語の選定に影響しないように意識しよう

このように、いくつかの基本的な法則を頭に入れればあまり迷うことなくコーディング中に英語を選定することができます。また、後々に間違いを引き起こし、事後処理に時間を使うというような間違いを引き起こすというのがワーストケースですので、より正確な意味を考えるのに時間を費やすのはそれなりに価値があることです。あなたの現場の英語も見直してみてはいかがでしょうか?

*1:なお本記事に関してネイティブスピーカーのレビューをしていただいています。

*2:タイムテーブルのように時間が定まっている場合はends(第一人称単数)を使用し、日時を表す時はそれに続くatを使用します。

*3:参考

*4:なお、Better specs(http://betterspecs.org/)がとても参考になりますのでチェックしてみてください。

*5:round tripは往復(行程)という意味です

よく言われる「施策を数字で」というやつについて

$
0
0

新規広告開発部の大野です。今回は、「目標を達成するための施策を数字で考える」ということについて、普段やっていることを書きます。

施策の評価に関しては Rを使うなどいろいろなノウハウがありますし、Web上の行動改善などはそれはそれで、世にノウハウがたくさんあります

今回は、例えば、期初に事業目標を決めた次のステップとして、全体の優先度を決めるあたりの段階の話をしましょう。

分解: 数字から取り組む施策を決める

まず、どの施策をするかを決めるわけですが、必ず、最初に目標を因数分解しています。

広告なら「収益 = 単価 × 在庫 × 販売率」といういつもの式があるので、これが元です。例えば

  • 単価がCPCなら在庫はクリック回数。つまり、imp(表示回数) × CTR
  • 単価が表示なら、在庫は単純に imp

となります。で、販売方法によって、どの項をあげるのが有効か、つまり、現実的に伸びるか、どのくらい伸び代があるかを評価し、あげたい項を決めます。

例えば、impは「媒体成長 x 枠数」に分解できるので、我々の努力としては枠数という係数を押し上げることになりますし、CTRであればクリエイティブ評価とかターゲティングとなりますね。

あと、作戦として単価をグッと下げていいなら、impに関して、外部の在庫を仕入れることで量を取る兵糧調達もできます。技術的にいえば、アドネットワークを作るということですね。

あげたい項に関して施策をリストアップし、優先度を評価します。僕は優先度評価に cost-worth評価を使います。

因数分解の仕方はいろいろあり、例えば「客数 x 客単価」とした場合には、客単価の分布変化を変えるために何をすればいいか、とか分解の方法で視点をガラッと変えられるので、多角的な分析をしやすいのも因数分解の良さですね。

落とし穴:前提条件と投資

因数分解の次は、施策が成功する前提条件・必要条件を必ず評価します。

例えば、前の例でCPCで「単価」項を上げると決めたとしましょう。

  • 単価を向上させるためにはオークションプレッシャーが必要である
  • オークションプレッシャーが成立する必要条件として
  • 獲得単価(CPA)の低下があるのでこれを考える

という感じです。このとき、CPA向上 → オークションプレッシャーを実現するための施策を別途考えて走らせておく必要があります(十分条件を整えておく)。

また、数字上効果があるように見える施策でも以下のような罠があったりするので要注意です。

  • 前提条件として実現不可能なものが盛り込まれたり、現実的に調達不可能な投資が要求される
  • 例えば単価が上がっても在庫が積めないとか、特定の項しか向上できなくて収益は低い
  • 他の施策のパイを奪ってしまう

特に、前提条件が何段にも連なるようなものは投資も不確実性も大きくなりがちなので注意する必要が有ります。

とはいえ

という感じで、すべての施策が同じように数字で評価されるのが理想です。ただ、それを優先して、本来芽がありそうな施策が絞られると元も子もありません。

そもそも明確な数字が出せないけど優先度が高いものもあります。広告だと販売戦略なんかはそうなることが多い気がします。代理店に成果ダッシュボードや管理画面解放しようみたいな話ですね。

あくまで全体収益の向上が目的なので、数字評価にこだわりすぎない。というのは要所要所で振り返りつつ大事にする必要があると思ってます(元も子もないですが……)。

ただし、数字評価を諦める時は、関係者を腹落ちさせるのが大変なので、そこをがんばることになります。そして、この事実から次の3つの教訓を得ることができます

  • 数字にすることに時間を使うよりは関係者を説得することを優先したほうが良いことが多々ある
  • 気持ち先行の施策が無駄に走ることを防ぐために「関係者」は説得されるハードルを日頃から上げておく
  • 基本は数字評価だけどそれだけなのもよくないよねという空気をチームに醸成しておく

なにより、自分がやりたいことは会社を利用する気持ちでやったほうがいいですね。

僕はずっとそうやって仕事をしてきましたが、一緒に働く仲間を切に募集しております。応募フォームからぜひお願いします!

Androidアプリを新規リリースする際のあれこれ

$
0
0

こんにちは、投稿推進部の吉田です。 少し前に、お料理アルバムという「日々の料理を写真を記録する」ためのアプリのリリースしました。初めて会社のプロダクトのリリース作業を経験して、色々と学びがあったので共有したいと思います。

見出し

  • releaseビルドをCIで作成する
  • releaseビルドから不要なモノを排除する
  • Google Playストアのリファラを活用する
  • Google AnalyticsとGoogle Playストアの連携をする
  • リリース直前チェックリスト

releaseビルドをCIで作成する

リリース版apkはコマンドから生成できる環境を整え、CIで作成するのがお薦めです。
CI上でのビルドすることで、ビルド手順の共有が不要になり、開発チームの誰でもボタン一つでビルドが可能になります。
また、ビルドをCIに任せることで、keystoreとパスワードを開発者全員に共有する必要がなくなるので、セキュリティ的にも少し安心できます。

ここからは、コマンドでビルドするための手順を紹介します。
まずkeystoreを作りましょう。Android Studioからも作れますが、keytoolコマンドを利用することも可能です。

keytool -genkeypair-aliasキーのエイリアス -keyalg RSA -keysize2048-sigalg SHA256withRSA -keystoreファイル名 -storetype jks -validity365000

keystoreはデバッグ用とリリース用の2種類を用意しましょう。
keystoreの置き場ですが、デバッグ用はレポジトリに含めて問題ないので、appモジュール以下に配置しましょう。
リリース用のkeystoreはホームディレクトリ以下のandroidディレクトリ内に配置したと想定して、以後は説明します。

今作成したkeystoreを使って、コマンドからreleaseビルドを作成するために、次の設定をandroid以下に追加します。

android {
  // ここから追加
  signingConfigs {
      debug {
          storeFile = file('./debug.keystore')
          storePassword = 'debug_password'
          keyAlias = 'app_alias'
          keyPassword = 'debug_password'
      }
      release {
          storeFile = file('./release.keystore')
          storePassword "release_store_pass"
          keyAlias "app_alias"
          keyPassword "release_key_pass"
      }
  }
}

signingConfigsにkeystoreの情報を追加したことで、コマンドからreleaseビルドの生成が可能になりました。
しかし、これではbuild.gradleにrelease用のkeystoreやパスワードが直書きされていて、微妙です。
そこでreleaseの設定をgitなどでバージョン管理を行わない別ファイルに切り出しましょう。

先ほどのbuild.gradleからreleaseの設定のみを切り出したものが以下になります。
このファイルをrelease.gradleとし、プロジェクトのルートディレクトリに配置します。
この際に.gitignoreの設定などでバージョン管理の対象から外す操作も同時に行いましょう。

signingConfigs {
    release {
        def HOME = System.getenv()["HOME"]
        storeFile file("${HOME}/android/release.keystore")
        storePassword "release_store_pass"
        keyAlias "your_app_alias"
        keyPassword "release_key_pass"
    }
}

次にbuild.gradleからrelease.gradleを呼べるようにしましょう。
signingConfigsの中でrelease.gradleの設定を読み込み、手元にファイルがない場合は、 debugビルドの情報をreleaseビルドにも適応するように書き換えたものが、以下になります。

signingConfigs {
    debug {
        storeFile = file('./debug.keystore')
        storePassword = 'your_store_password'
        keyAlias = 'your_app_alias'
        keyPassword = 'your_store_key_password'
    }
    def releaseSettingGradleFile = new File("${project.rootDir}/release.gradle")
    if (releaseSettingGradleFile.exists()) {
        apply from: releaseSettingGradleFile, to: android
    } else {
        release {
            storeFile = debug.storeFile
            storePassword = debug.storePassword
            keyAlias = debug.keyAlias
            keyPassword = debug.keyPassword
        }
    }
}

これで、./gradlew assembleReleaseを叩くとリリースビルドが作成されるようになります。

releaseビルドから不要なモノを排除する

開発の際にはデバッグに便利なライブラリを入れて開発することがあると思いますが、 debugCompileとして依存に入れておくと、それらの実装がreleaseビルドのapkに含まれなくなります。
今回はFacebook社が提供しているStethoというツールを例にdebugCompileの使い方を説明します。
まずbuild.gradleのdependenciesにStethoを追加します。

dependencies {
    debugCompile 'com.facebook.stetho:stetho:1.1.1'
}

Stethoの初期化は通常Applicationに書きますが、main以下のApplicationを継承したクラス(CusotomApplicationとします)には、依存関係上Stethoに関するコードは書けません。
そこで、app/src/debug/java以下にCusotomApplicationを継承したDebugApplicationを作成します。(debug以下のpackage構造はmainと合わせています)

publicclass DebugApplication extends CustomApplication {

    @Overridepublicvoid onCreate() {
        super.onCreate();
        setUpStetho(this);
    }

    privatevoid setUpStetho(Context context) {
        Stetho.initialize(
                Stetho.newInitializerBuilder(context)
                        .enableDumpapp(Stetho.defaultDumperPluginsProvider(context))
                        .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context))
                        .build());
    }
}

デバッグ時はDebugApplicationが参照されるように、app/debug以下にAndroidManifest.xmlを作成しApplicationにDebugApplicationを指定することで、 debugビルド時はManifestを上書きすることが出来ます。

<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.cookpad.android.cookingphotoalbum"><applicationtools:replace="android:name"android:name=".application.DebugCookingStoryApplication"></application></manifest>

Google Playストアのリファラを活用する

累計/日毎のインストール数はplay developer consoleで確認することができますが、 どの導線からのインストールなのかは知ることができません。 実はGoogle Playストアのリンクにクエリパラメーターにreferrer属性を追加すると、アプリ側でリファラを受け取ることが可能です。

https://play.google.com/store/apps/details?id=com.cookpad.android.activities&hl=ja&referrer=xxxx

受け取り側はアプリのReceiverとして定義します。 GoogleAnalyticsやMixpanelを使っていると、リファラを受け取るためのReceiverが定義されているのでそちらを利用しましょう。 自分でReceiverを定義した場合このような実装になります。

publicclass ReferrerReceiver extends BroadcastReceiver {

    @Overridepublicvoid onReceive(Context context, Intent intent) {
        Bundle extras = intent.getExtras();
        if (extras != null) {
            String referrer = extras.getString("referrer");
            if (referrer != null) {
              // post event
            }
        }
    }

}

AndroidManifestの定義はこのようにします。

<receiverandroid:name="com.your.package.name.ReferrerReceiver"android:exported="true"><intent-filter><action android:name="com.android.vending.INSTALL_REFERRER"></action></intent-filter></receiver>

Receiverの検証をするためには、com.android.vending.INSTALL_REFERRERイベントを発火させることが可能です。

adb shell am broadcast -a com.android.vending.INSTALL_REFERRER --es referrer debug_test_event

Google AnalyticsとGoogle Playストアの連携をする

Google AnalyticsとGoogle Playストアを連携させると、Google Analyticsの集客タブで、下の画像のようにGoogle Playストア内の行動まで把握できるようになります。 ストア内の離脱率がわかるようになるので、Play Developer Condoleのストア掲載文言のA/Bテスト機能とあわせると、掲載文言の改善が行えそうです。

f:id:kazy1991:20150904153155p:plain (引用元: How Google Analytics helps you make better decisions for your apps | Android Developers Blog)

Google AnalyticsとGoogle Playストアの連携の条件は、

  • 双方のアカウントが同じメールアドレスであること

なので、Google Playストアに登録しているアカウントがGoogleAnalyticsの編集権限を保持した状態で連携の操作をする必要があります。 なお設定は、アナリティクス設定>すべての商品から可能です。

リリース直前チェックリスト

最後にリリース日が近づいてきたら確認したいリストを、まとめました。
項目は少ないですが、よろしければご活用ください。

* [ ] PlayStoreの文言の準備(タイトル[30文字], 簡単な説明[80文字], 詳細な説明[4000文字])
* [ ] PlayStoreのストア用画像の準備(スクショ2枚, 高解像度アイコン[512x512], 宣伝用画像 [1024x500])
* [ ] Bugレポート系ツール(Crashlyticsなど..)の動作確認
* [ ] ログ収集系ツール(Mixpanel, GoogleAnalyticsなど..)の動作確認
* [ ] 通信するAPIが社外ネットワークからアクセス可能か確認
* [ ] デバッグ用のActivityがmainのAndroidManifestに含まれていないか確認
* [ ] release用のkeystoreでビルドされているか確認
* [ ] 利用しているOSSのオープンソースライセンスが明記されているか確認
* [ ] applicationIdの確認

applicationIdについて補足すると、applicationIdはPlayストアのURLにそのまま利用されます。 プロジェクト名をリネームした際に、build.gradle以下のdefaultConfigのapplicationidを変更し忘れると、 想定していたURLと違うURLでGoogle Playストアにリリースされてしまうのでお気をつけ下さい。

定番メニューを提案して検索体験をより良くする

$
0
0

クックパッド検索・編成部の須藤耕平です。

先日、「定番提案」(と呼んでいる)機能をスマートフォン版のクックパッドにリリースしました。
本エントリでは、この機能を開発するにあたって考えたことや、形にする上で工夫した点などを紹介させて頂きます。

f:id:sudokohey:20150907185918p:plain

ひき肉の検索結果に表示される定番メニュー

検索成功率を追う

検索チームでは、サービスの満足度を図る指標として検索成功率という数値を追っています。 検索成功率とは、特定の条件を満たして終了する「検索成功セッション」が全セッションに占める割合を示したもので、この数値が高いほど、目的のレシピに到達し易い検索体験を提供できていると考えています。

レシピが決まりにくいキーワードから利用シーンを推測する

クックパッドの検索窓に入力されるキーワードは、食材名、料理名、その他の3つに大別されます。 その他に分類されるキーワードの多くは、「ダイエット」「ランチ」「誕生日」といった目的を示す言葉であり、これらは他と比較して検索される頻度こそ少ないものの、検索成功率が著しく低いことが分かっていました。

f:id:sudokohey:20150908091803p:plain

目的系のキーワードで成功率が低い理由としては、探している本人がそもそも「正解」を理解していなかったり、誕生日パーティをするので食卓全体をコーディネートしたいといった、ぼんやりとした気持ちで検索に臨んでいるためと推測できます。

そこで、すでに検索を開始しているユーザーの動線上にどのようなサポートがあれば、ぼんやりした状態を解消できるかを考えました。

検索モードと機能をマッピングする

人が情報を探し出す状況に於いて、複数のモードが存在するという考え方があります。 *1分かりやすくレシピを探し出す状況を例として添えると、以下のようになるでしょうか。

  • 認識不足モード (例:ダイエットに関心があるが、何が有効かなど、全く知識の無い状態で探している)
  • 調査探索モード (例:冷めても美味しく食べることができるお弁当のおかずを探している)
  • 既知項目検索モード (例:簡単な回鍋肉の作り方を探している)
  • 再探索 (例:以前に作ったxxさんのyyというレシピを探している)

一口に検索するといっても背景には様々な文脈がありますが、このような分類に当てはめるて考えると、先の「ぼんやりした状態」は認識不足モードに該当すると言えそうです。

また、既に提供している機能の面から、上記の各モードに対してどのようなサポートができているかを考えてみました。

f:id:sudokohey:20150907190316p:plain

例えば 再探索モードであれば、 MYフォルダという機能を使ってお気に入りのレシピを保存することができますし、既知項目検索モード であれば、「簡単 回鍋肉」の1番人気のレシピを見ることで、ぴったりのレシピに辿り着くことができるかもしれません。
一方で、対象に対して明らかに認識が不足しているような状況を想定すると、すでに検索を開始してしまったユーザーの動線上には、それを解決するための十分な機能がありませんでした。

検索対象の概要を俯瞰して認識不足を解消する

例えば、「ランチ」と検索すると、約26,000品のレシピが検索結果に返るのですが、この状況から自分にぴったりのランチメニューを探し出すのは容易ではありません。 そこで、ぼんやりと「ランチ」向けのメニューを探したいと考えている状況に対して「ランチ」の定番メニューを提案し、その認識を補完することで検索をサポートできないかと考えました。

この機能を利用した場合の想定される遷移は以下の通りです。

f:id:sudokohey:20150907190346p:plain

  1. 「ランチ」と検索する(26,000品のランチ向けレシピが返る)
  2. 提案されたチャーハン、おにぎらず、どんぶり...といったメニュー候補の中から、「パスタ」を選択
  3. 提案されたペペロンチーノ、クリームパスタ、和風パスタ...といったメニュー候補の中から「冷たいパスタ」を選択
  4. 提案されたトマト、生ハム、アボガド...といった冷たいパスタ向けの食材候補の中から「トマト」を選択
  5. 食べたいものが具体化し、検索対象が当初の1/10(2,500品)にまで絞り込まれる

こような遷移を辿ることで、「ランチ何にしよう?」というぼんやりした認識から、「トマトを使った冷製パスタが食べたかったんだ!」という具体的な状況に到達させることができれば成功です。

ちなみに、2,500品でも多くて決めきれないとは思いますが、この機能の役割はモードの移行を起こすことであり、絞り込まれた特定のメニューのリストから最後の1品を選ぶにあたってのサポートは前述した別の機能が担います。
検索セッションのような流れのある体験を改善する場合には、単一の機能で全てを賄うのではなく、状況に応じて段階的に手を差し伸べるようなコミュニケーションをデザインすることが大事だと考えています。

ユーザーの期待を先回りして提案する

提案の種類にはメニュー名を返すケースと、食材名を返すケースの2パターンを用意していますが、その判定には関連キーワードという既存の機能を利用しています。
関連キーワードは、あるキーワードで検索を始めたユーザーが、次にどんな言葉に書き換えて検索したかを集計したもので、例えば、「カレー」で検索した場合の関連キーワードは以下のようになります。

f:id:sudokohey:20150907190438p:plain

「隠し味」「リメイク」とった単語もありますが、全体に占める割合としてはメニュー名が高いことから、提案として期待される内容はメニューであると判断し、以下のようなメニュー群を候補として返しています。

f:id:sudokohey:20150907190505p:plain

「アヒージョ」と検索した場合は、関連キーワードに食材が多く含まれるため、提案の候補として食材を返します。

f:id:sudokohey:20150907190536p:plain

これを動的に判定することで、「パスタ」であればメニュー名、「冷たいパスタ」であれば食材名といったように、キーワードに応じてユーザーが期待する提案を先回りして返せるように工夫しています。

ちなみに、面白い提案の例としては以下のようなものがあります。

f:id:sudokohey:20150907190608p:plain

冷蔵庫にありそうな食材と、副菜っぽい小鉢のサムネイルが並びます

副菜に困っても、冷蔵庫のよくある食材でちょっとした何かを作れそうです。
変わり種としては地名が面白いです。

f:id:sudokohey:20150907190748p:plain

名古屋名物いろいろ

f:id:sudokohey:20150907190815p:plain

沖縄の定番料理

沖縄旅行に行って食べたレシピを家で再現したいけど、あれ何ていう料理なんだろ?なんていうときに便利かもしれません。

成功率は向上したか?

技術的な詳細は長くなるので割愛しますが、数多くのプロトタイプとABテストによる検証を繰り返した結果、特定の状況(クエリ)に於いては有意な成果を確認できたものの、残念ながら全セッションを対象とした全体の検索成功率に対して大きなインパクトをもたらすことはできませんでした。
が、そんなことは日常茶飯事で、この程度で肩を落としていてはクックパッドのサービス開発は務まりませんし、ここから得られた知見で、明日の料理をもっと楽しくできる!という内なる炎を燃やし続ける能力が何より大事です。

目下、提案内容の質を高めるべく継続してプロジェクトは進んでいますし、検索成功率そのものを算出するロジックもより精緻なものへ改善を図っています。
また、より多くのユーザーにこの体験を提供すべく、アプリ(ios, android共に)を含めた全プラットフォームへの横展開も絶賛進行中ですので、このエントリを読んで少しでも興味を持って頂けたエンジニア・デザイナーの方がいらっしゃいましたら、ご応募お待ちしております!

*1:オーストラリアのインフォメーションアーキテクトDonna Maurerによって提案された4つの情報探索モード。UX系の本とかを紐解くと時々出てきます。

夏の技術職インターンシップ講義資料公開

$
0
0

こんにちは!クックパッド編集室メディア開発グループ長の @yoshioriです。

このまえ夏の技術職インターンシップの前半の開発講義・課題部分が終わったのでさっそく公開しちゃいます!

ちなみにこのインターンの対象者はプログラミングはわかるし自分で(授業とかではなく)コード書いている人なので超初心者向けでは無く、少なくともひとつ以上の言語でプログラミングが出来る人向けです。


一日目

TDD + git 編(@yoshiori

講義初日なのでまずは簡単に肩慣らし & 開発の基礎の部分として TDD と git で始めました。 git については軽く説明し TDD は基本のテストファーストで進めて行きました。 ちゃんと何かをするたびにテストを実行し、メッセージを見れば次にすることが分かるというのを体験してもらい、GREEN が良くて RED が悪いのではなく、GREEN を想定しているのに RED になったり RED を想定しているのに GREEN に成るのが良くないというのを学んで貰いました。 翌日以降は静的型付言語 + IDE での開発になるのでテストによって typo とかも拾えるというのを自分で経験してほしく自分で写してもらいながら進めていきました。

Rails 編(@yoshiori

Rails の講義では引き続きテストファーストで進めました。 フレームワークの使い方を覚えてもあまり意味が無いと思い、REST 的な部分を重点的に説明しました。具体的には基本となる「resource 操作は Path と method でユニークになる」というのを実感してもらうために telnet で実際に http をしゃべるところから始めました。 テストファーストで進めると Rails はエラーメッセージが親切なのでホントにテストに導かれながら開発するような感覚になれるのを実感してもらいました。 また JSON を返す API 部分も作り、なんとか heroku へのデプロイまで含めて行いました( 作ったアプリは翌日以降のクライアント開発で使うため)。


二日目

Android アプリ 編 (@101kaz + @tomorrowkey)

Androidの講義では前日に作ったRailsアプリケーションのAPIを使うクライアントアプリを作りました。 Androidアプリが起動してから終了するまでの流れや、レイアウトの作成、イベントドリブンなプログラムの書き方、サーバとの通信方法など、Androidアプリを作る上で最低限必要な知識を説明しました。 講義の流れとしては、基本的な画面だけ講義で一緒に作り、残された画面は各々が思う使いやすいアプリを考えながら開発してもらいました。 みなさんとても優秀だったので、少しのアドバイスで各々が思うユニークなアプリを作ることができました。

資料: https://github.com/cookpad/cookpad-internship-2015-summer/blob/master/android.md


三日目

iOS アプリ 編 (@slightair)

iOS アプリの講義では簡単なアプリの実装を進めながら iOS アプリ開発の基本を学んでもらいました。 Rails の講義で作成した Web アプリの API を実行し、同様の機能を持つ iOS アプリを開発しました。 開発に使ったプログラミング言語は Swift 1.2 です。

簡単なアプリとはいえ、画面要素の配置や画像の表示、APIリクエストの送信など、一般的な iOS アプリ開発に必要な知識にひと通り触れています。 iOSアプリの開発ではコードを書くだけでなく Xcode 上の操作が必要になってくるので、プロジェクターに画面を映して操作を見せながら講義を進めていきました。 AutoLayout の設定でつまづいてしまう人がたくさん出てしまいましたが、講義の終わりには全員が目的のiOSアプリの形へ仕上げることができました。

資料: https://github.com/cookpad/cookpad-internship-2015-summer/blob/master/ios.md


四日目

サービス開発編 (@ryo_katsuma)

サービス開発の講義では、クックパッドでのサービス開発に対する考え方と、その実践方法についてのエッセンスを学んでもらいました。

「サービス開発のおいて正解は誰にも分からないから、学びのある失敗を繰り返し、その中から成功に辿り着く」リーン・スタートアップに基づいた考え方や、ユーザーインタビューの仕方や効率的なMVPの作り方、検証数値の説明などを説明しました。

演習課題では実際にサービス企画を行ってもらいました。 本来はユーザーインタビューによる課題発見、MVPによる検証、その結果を元にした新たな仮説を立てるところまでサイクルをひと通り回したかったのですが、今回は時間の関係上そこまでは実現できませんでした。このあたりの調整は次回の課題になりますね。

機械学習・自然言語処理編 (@mrkn+原島+@chezou)

機械学習・自然言語処理の講義では、機械学習の入門的な話と自然言語処理の概要をクックパッドの事例と共に学んでもらいました。

機械学習の講義では、機械学習とはなにかという話から、教師あり学習、教師なし学習、そして評価と過学習の話など基礎的なところを解説しました。

自然言語処理の講義では、形態素解析や構文解析などの基礎解析から検索エンジンや自動翻訳などのアプリケーションまで一通り解説しました。実習では、形態素解析器を使って、レシピのタイトルを解析・マイニングしてもらいました。

speakerdeck.com

演習課題としてクックパッドとWikipediaのデータから作ったword2vecのモデルを比較して、レシピサイト特有の類義語などの言語現象を探求してもらいました。

www.slideshare.net

五日目

プログラミングパラダイム編(青木峰郎)

「プログラミングパラダイム」についてとかふわっとしすぎだろ! ていうかプログラミングパラダイムを語りたいなら 最低でも各パラダイム1言語は知らないとお話にならないし、 言語処理系くらい普通に書けてあたりまえだろ!

ということで、この講義では言語処理系を作ってもらいました。 1日という短期間である程度本気の処理系をさわれるように、 JavaScriptのコンパイラ(未完成)とVMをこちらで用意し、 コードジェネレーターだけを書いてもらう方式です。 ソース言語は多くの人が知っている言語のほうがよいのでJavaScript。 ターゲットは機械語ほどつらくないバイトコードVMとしました。

言語処理系をさわったことがないというインターン生は意外と多く、 VMスタックのpush・popが合わなくてVMに怒られたり、 謎の最適化に悩まされたりと、想定通りのつらいめにあってくれました。 人生一度くらいは全力でスタックと戦う経験をするのも悪くないのではないでしょうか。


今回こういったガッツリ講義などをするインターンシップは初の試みでした。 色々反省点もありますが、また来年も出来ればいいなと思っています。


安全なリリースのために心がけていること

$
0
0

こんにちは。会員事業部の高田です。今回は安全にリリースをするために、アプリケーションエンジニアとして心がけていることについて書きます。

クックパッドではなるべく早くユーザーに価値を届けることを大切にしているため、1 日に何度も安全にリリースできるしくみや、リリース後に発生したエラーにすぐに気付けるしくみがあります。しかし、アプリケーションレイヤーの問題はアプリケーションエンジニアが気をつけなければいけません。私個人としては、少し気をつければ防げた問題を起こしてユーザーや会社に迷惑をかけてしまった経験があるため、注意深くなっています。

プロジェクトの大きさや目的はさまざまあり、早くリリースして検証したい施策などもあると思います。今回は慎重にリリースしたいプロジェクトの例として、有料会員の解約の前に特定のユーザーにのみクーポンを配布するという架空のプロジェクトを元にお話します。

1. 準備

リスクを洗い出す

まずは、避けるべきケースについて、ユーザーへの不利益や会社への不利益などの観点から洗い出します。

  • 文言に誤りがありユーザーの信頼を失う
  • クーポンが使えない
  • 解約したいのに解約できない
  • 対象でないユーザーにクーポンが配布される

早い段階でリスクについて考えておくと、リスクを避けるための行動の漏れを防ぎやすくなります。私は最近はプロジェクトが開始した段階で、GitHub Enterprise に専用のレポジトリをつくり、最初の issue として「リスクの洗い出し」を登録しています*1

ダブルチェック体制をつくる

開発者とは別の視点で動作確認をすることで、問題を事前に防ぐことができます。

体制が整っていればさほど気にすることはないですが、初めてのメンバーとプロジェクトを進める場合やメンバーが一人の場合には、スケジュールから漏れてしまわないよう、そもそも「ダブルチェックを行う」ということを理解してもらうこと、「ダブルチェックを行う人」を明確にしておくことが大切です。

ダブルチェックをする際のチェックリストをつくるには、洗い出してあるリスクが役に立ちます。

2. 開発中

テストを書くことやレビューをすること、実際に動かして動作確認することは当然ですが、もう一つ心がけたいのは本番環境で動作確認できないところを減らすことです。

私は本番環境での動作確認がしにくく省略してしまったためにバグを発生させてしまった経験があり、今では「本番環境で動作確認できないところにはバグがある」と思っておくくらいがいいと考えています。

今回の例だと、実際にクーポンを使えることや退会できることを確認したいものです。本番環境で対象ユーザになることが難しければ、Chankoのような対象ユーザを管理しやすいライブラリを使うのもよいですし、単にテストユーザ用の条件分岐を追加するのもよいでしょう。

3. リリース前

リリース日は慌てないようにします。リリース予定時間の前後にミーティングが入っていたり、翌日リリース予定の別タスクを抱えていたりするような場合は、ワーストケースが発生しても早めに気づいて対応する余裕があるか考えてみます。余裕がないようであれば、責任をもって関係者に相談して余裕をもつようにします。

4. リリース後

本番環境での動作確認だけでなく、リスクのある事象が発生していないかを確認します。

  • 退会するユーザがリリース前と比べて増えたり減ったりしていないか
  • クーポンが実際に使われているか
  • アクセスログを見て、対象でないユーザがクーポン配布ページに進んでいるということがないか

など、確認できることはいろいろあると思います。

まとめ

私が失敗を通して学んだことを元に、安全にリリースするためにこころがけていることについて書きました。

はじめに触れたとおり、プロジェクトの特徴はさまざまなので、ここで書いたような手順がまったく必要ない場合も、もっと慎重にしなければいけない場合もあると思います。そのような場合でも、考えるきっかけとしてなにかの参考になれば幸いです。

*1:プロジェクトの目的や有効性などについては十分議論されていることを前提としています

App Transport Securityとネットワーク広告

$
0
0

新規広告開発部の松本です。

本日午前2時のAppleの発表イベントにて、iOS 9が9/16にリリースされる事が明らかになりましたね(GM版は本日リリース)。
このiOS 9には様々な機能追加がありますが、iOSアプリにネットワーク広告を設置されている方はApp Transport Security(以下ATS)に気をつける必要があります。

ATSとは

公式ドキュメントによると、

App Transport Security is a feature that improves the security of connections between an app and web services. The feature consists of default connection requirements that conform to best practices for secure connections. Apps can override this default behavior and turn off transport security.

Transport security is available on iOS 9.0 or later, and on OS X 10.11 and later.

とあります。
一言で言えば「デフォルトではAppleの推奨する設定のHTTPS通信を強制する」ということです。
詳しくは上記公式ドキュメントやWWDCのセッション動画(Safariのみ再生可、書き起こしはこちら)、Developers.IOの記事等を参照すると良いでしょう。また、弊社エンジニアのブログ記事も参考になります。

ATSとネットワーク広告

このようにiOSアプリおよび通信先のサーバの対応が強く求められているのですが、個々のiOSアプリ開発者だけではどうにもならない通信先もあったりします。
本稿ではその具体例としてネットワーク広告を挙げてみます。

ATSを有効にすると、ネットワーク広告が表示されなくなる可能性があります*1。 ですので、現在使用しているネットワーク広告がATSか対応かどうかを調査し、非対応なのであれば「そのネットワーク広告の使用を諦める」か「ATSの設定を変更する」かを選択しなければいけません。

現在、各社SDKの状況は「既に対応済み」「まだ対応していないが対応予定」「アナウンスがないので問い合わせる必要がありそう」等様々ですが、オープンにアナウンスされている例としてGoogleMobileAds SDKを見てみます。

Google Ads Developerブログによると、

This change may need your action if you are developing with the Google Mobile Ads SDK and building an app against the iOS 9 SDK.

(中略)

While Google remains committed to industry-wide adoption of HTTPS, there isn’t always full compliance on third party ad networks and custom creative code served via our systems.

とあります。GoogleMobileAds SDKではまだATSに対応しきれていないため、このままでは広告を取得できなくなるようです。

また、

To be clear, developers should only consider disabling ATS if other approaches to comply with ATS standards are unsuccessful. Apple has provided a tech note describing different approaches, including the ability to selectively enable ATS for a list of provided HTTPS sites.

とあります。GoogleとしてもできるだけATSを無効にするのは避けて欲しいが、他に打つ手が無い場合、選択的にATSの設定を変更することを検討して欲しいようです。

(※ ちなみに上記ブログ中で画像とコード付きで示されている対策方法はATSを完全に無効にするため推奨されません。)

ATSの設定変更はInfo.plistに値を入力することで行います。 以降では、ATSを有効にするドメインだけを列挙する方法と、無効にするドメインだけを列挙する方法を記載します。

ATSを有効にするドメインだけを列挙する方法

NSAllowsArbitraryLoadsで例外を除く全てのドメインでATSを無効にします。
また、例外としてexample.comに対してのみNSExceptionAllowsInsecureHTTPLoadsNO, <false/>になっています。

選択的に有効化

<key>NSAppTransportSecurity</key><dict><key>NSAllowsArbitraryLoads</key><true/><key>NSExceptionDomains</key><dict><key>example.com</key><dict><key>NSExceptionAllowsInsecureHTTPLoads</key><false/></dict></dict></dict>

自社APIのエンドポイントはATS対応したいけれどATS非対応のネットワーク広告も使いたい・・・という場合は、この方法を取る必要があるようです。

ATSを無効にするドメインだけを列挙する方法

今回のネットワーク広告の件には関係ありませんが、選択的有効化の逆も記載しておきます。

ATSはデフォルトで有効なので、こちらはNSAllowsArbitraryLoadsのような指定がありません。
また、例外としてexample.comに対してのみNSExceptionAllowsInsecureHTTPLoadsYES, <true/>になっています。

選択的に無効化

<key>NSAppTransportSecurity</key><dict><key>NSExceptionDomains</key><dict><key>example.com</key><dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/></dict></dict></dict>

ATSを部分的に無効にして大丈夫なの?という話

これに関しては弊社エンジニアのブログ記事を参照したいと思います。

諸説あるとは思いますが、ATS を切ることそのものが危険な状態に繋がるわけではなく、こうしたものをあまり理解せずに切ってしまったり、切ったまま対応を考えないといったことが危険な状態を招くと考えています。

ATSの設定変更によりHTTPS化をサボる訳ではなく、常に自分のアプリの通信先に気を配り、最終的には全てのドメインでATSを有効にしても問題ない形になるよう努める必要がありますね。

また、ネットワーク広告に限らず、皆さんがiOSアプリからアクセス可能なエンドポイントを公開していましたら、ATS対応を検討すると皆さんも周りの人も幸せになれるかもしれません!

参考

*1:ATSはXcode 7以降でビルドしたアプリで有効になるため、Xcode 6を使っている場合は有効になりません。ですので、何もしていないからネットワーク広告が表示されなくなっちゃう!ということは(最後にアプリをリリースした時にXcode 6を使っていれば)ないはずです。

調整の心得

$
0
0

会員事業部の森田です。

はじめに

この記事は、クックパッドと同じような200~300名規模の組織で働く、「最近調整が多くてコードを書く時間がないなぁ」と思い始めた30代エンジニアの方を対象として、調整の負担を軽減するために私が日々考えていることをまとめたものです。

組織における分業と調整

組織に所属する人たちは協力して組織目標の達成を目指します。みんなで同じことをしてもしょうがないので、必然的に役割を分担(分業)をします。分担した仕事はなんらかのタイミングで統合する必要があります。その統合が調整です。つまり分業と調整はセットです。じゃどういう分業があるのかといえばそれは組織構造によります。今回は私達が採用している事業部別組織下*1での調整の話をします。

分業の種類

事業部別組織では垂直と水平の2つの分業が存在します。それぞれに少し毛色の違う調整が発生するわけですが、いくつかのことを意識することで調整の負担を減らすことができます。減らしても大変ですけどね。最近はいかに無駄なく調整をこなしてコードを書く時間を確保するかがエンジニア35歳定年説に打ち勝つ鍵だと思いながら働いています。では、それぞれの分業ごとに説明します。


垂直分業

まずは垂直分業の調整についてです。垂直分業とはつまり上司と部下の分業です。そこで発生する調整は「部署内において今期はだれがどういう仕事をするか」といったものになります。今回は上司との調整を前提として話をします。

上司の責任範囲

垂直分業では部下は上司の責任範囲の一部を担います。根本的におかしなことにならないためにも上司の会社での責任範囲を確認し理解します。

自分の責任範囲

自分の経験や得手不得手などを考慮し、責任範囲を明確にします。双方の腕の見せどころです。仕事に慣れないうちは「目的Aのための施策Bの効果を検証する」という責任だったものが、慣れるに従い「目的Aのための施策を考え検証する」になり、しまいには「目的Aがただしいか考え、正しくなければ何が正しいかを示し適切な施策を考え検証する」と変化していきます。

ここの認識で齟齬があると非常に辛いのでしつこく確認します。意識して3倍増しです。

権限(制約)

自分のもつ権限を確認します。権限を例えると「自社のトップページのこの領域を自由に使う」であり、制約を例えると「一人で頑張って」などです。この2つはひとつの物事を逆から示しています。また、もし部下を持つなど、人に関する権限を割り当てられた場合は、「権限受容説」を前提にしたコミュニケーションをとることが円滑に進める秘訣だと思います。

権限と責任のバランス

この2つのバランスがとれていないと誰かが苦しい思いをします。権限が足りなければ自分が、権限が多すぎれば周りが苦しみます。

失敗(成功)の定義

たまに失敗の定義がない、つまり失敗しない責任になっている事があります。そうなると自分でも何をしていいのか分からなくなります。責任も権限もあるのに失敗しようがない場合は何かがおかしいので確認をします。

確認の頻度

ここまでを丁寧に進めたとしても、お互いの考えは多かれ少なかれ食い違いっています。人間、簡単にはわかりあえません。そのため責任範囲がきまり、仕事を開始してしばらくは、なるべく短い間隔で話をし、お互いの考えを同期します。うまくいけばすぐに「この頻度ではいらないのでは?」と双方が思うようになるので、段々と間隔をのばします。

対等な関係

当然ですが上司部下は垂直分業での役割の違いにすぎません。人としての上下関係ではなく構造による分業と考えそのなかで対等に話し議論をかさね調整します。

ちなみに、「そもそも意見をいうことが苦手なんだよね」という方は「アサーティブネス」という概念が役に立つかもしれません。ほかでは少し前に流行った「アドラー心理学」ですかね。


水平分業

次に水平分業の調整についてです。水平分業とは会員事業部や広告事業部といった役割の分業です。水平分業の調整では複雑な利害関係が発生する場合が多々あります。そのような場合は各々が話し合い全体最適解導き出すよりも、上位の責任者に決めてもらうのが理想です。*2

しかしながら、現実ではそういった責任者がみつからなかったり、みつかっても入ってもらうことが難しいこともあるので、今回は利害をふくめた水平分業の調整を自分でなんとかする際の話をします。これはつまり、特定の領域に対して責任を持つ各々が、「全体最適」という本来であれば責任外の要素を考慮しながら頑張るという構造なので、垂直分業の調整より大変です。このような構造下では交渉の色が強めにでてしまうこともある程度は仕方がありません。

コンテキスト

自分の都合をはなすとき「Xという施策をしたいのですが」ではなく「Aというコンテキスト上でBという目的があり、そのためXという施策をしたのですが」と伝えることが大切です。目的を丁寧に伝える事は調整相手が全体最適を考える上で不可欠な情報です。

自信の程度

自分の施策にたいして「5%ぐらいの成功率かもしれない。それでもやりたい。」と思っているとします。これを説明しないと相手は「この人は大丈夫か」と心配になります。そのため、どれくらいの自信をもっているのか伝えます。たいていの人はコントロールされていないリスクに付き合いたくありません。

相手のコンテキスト

必要であれば「コンテキスト」「自信の程度」に書いた内容を相手から引き出します。

自分と相手の守りたいもの

双方の守りたいものを確認します。ここでいう守りたいもの、たとえば「施策の効果検証」であり「ユーザの体験」であり「短期的な収益」であり「合意形成のプロセス」などです。それらの情報から譲歩可能な点が何かを整理し確認します。同時に、ここを積極的に引き出す姿勢をみせることで「この人は私達の利害や目的、信念を尊重してくれないのではないか」という恐怖を取り除く意味もあります。人は怖くなると怒り出したり頑なになります。

早めの連絡

ギリギリで伝えると蔑ろにしていると思われます。そうではなく、ただまぬけなだけなのですが、勘違いされないためにも、早い段階で情報の共有を行います。それにより、思わぬアイデアが生まれてきたり、連携した仕事ができる事もあり、一石二鳥です。

記録

言葉で合意をとっていても、細部で食い違っていたりします。後に水掛け論にならないためにも、合意した内容を文章にし内容を確認します。


共通

最後に水平、垂直に限らず関係することを話します。

信頼関係

頑張って築きます。コミュニケーションコストは信頼関係に反比例します。

対立構造の排除

調整を対立構造と考えず、組織として良い答えを模索する場であると常に意識します。


その他の技術

調整を支える技術は他にもたくさんあります。その中には「人としてどうなの?」と思える危ういものもあります。興味があるかたは「すぐれた組織の意思決定」のパワーに関する記載や、「すぐれた意思決定」や「影響力の武器」などで示される社会心理学、行動経済学、認知バイアスに関する情報を御覧ください。これらは危ういことをしたい人に限らず、そういうことから身を守りたい人にも役立ちます。


まとめ

調整はつかれます。「コードだけかけたらどんなに幸せか。」と思い転職してきた同僚たちが結局調整をしているのをみるたびに、大変だなと思います。残念、組織で働いている以上調整は避けられません。避けたつもりでいても行き着く先はback numberの「こわいはなし」の世界です。という話を新婚の同僚にしたところ、「恋愛のほうが簡単だ」という見解をいただきました。今後も、調整を避けるのではなく、向き合いながらも効率よくこなすことで、コードを書く時間を確保していきたいと思います。

*1:厳密には事業部に加えてエンジニアはエンジニアとして水平にゆるく部門化されているマトリックス組織です。

*2:すぐれた組織の意思決定」に非常にわかりやすく書かれています。

朝Lint活動で細かな技術的負債を返済する

$
0
0

買物情報事業部の八木です。クックパッド特売情報のAndroid部分を担当しています。普段はクックパッドのAndroid版(以後、本体アプリとします)の開発プロセスの中で特売情報の機能を開発しています。

本エントリでは細かな技術的負債を解消する為に本体アプリの開発チームが行っている朝Lint活動を紹介します。

2年近く経つ本体アプリのコードベース

私が買物情報事業部に所属する前は本体アプリを1から書き直すチームで働いていました。書き直し始めたのは2013年10月からなのでそろそろ2年が経とうとしています。2年前に設計された本体アプリは現在ではおよそ17万行を越え、日々どんどん変更が加えられています。

それらの変更の中には残念ながら悪いコードが含まれている場合があります。テストしづらいコードやテストがないコード、レビューに対する場当たりな対応や緊急のbug fixのために追加された汚いコード、スケジュール上むりやりねじ込んだコード、minSdkVersionに対応するためのコード、deprecatedになってしまったAPIの利用、メンテナンスされなくなったライブラリの利用など細かな問題が日々増えていっています。

細かな技術的負債の弊害

技術的負債のうち大きなものはissueを立ててマイルストーンを設定し開発プロセスに乗せるのでそこまで致命的な問題にはなりません。どちらかというと以下のような性質のコードが長く残りがちです。

  • 古い実装方針だが動作は正しいため次に変更する時まで放置されている
  • テストが書けない状態だがテストが書ける状態にする為には影響範囲が大きすぎる
  • 機能追加の為に多くの変更が必要だが定形的なためコピペに近い変更で済んでしまい構造の再設計へのモチベーションが低い
  • Lint警告が出ているが問題は起こっていない

こうしたコードは正しく動作していて致命的な問題をあまり起こさないように見えるため修正へのモチベーションが低く大抵後回しになります(あるいは永遠に対応されません)。こうしたコードを残しておくリスクとして次のような事が考えられます。

  • 本体アプリに初めて変更を加える人が悪い見本を使ってコードを書いてしまう
  • 構造的な問題による新機能の追加コスト、既存機能の変更コストの増加
  • 可読性の低下と歴史的経緯を学習するコストの増加、学習不足によるバグの混入
  • 新しい方針を適用するハードルが上がる

これらは改善したいけど手を付ける暇がない問題として存在しつづけていました。

潜在的なリスクを可視化する

ある時の振り返りで「間違っているけど動くコードが放置されていたため問題が起こった」というリスクがそのままバグになって現れたようなProblemが出てきました。これに対して「まずは潜在的なリスクを可視化する」というTryを行うことにしました。

具体的にはbuild.gradleにLint警告を表示するコンパイルオプションを追加して怪しい警告を見逃さないようにしよう、というものでした。

allprojects {
  gradle.projectsEvaluated {
    tasks.withType(JavaCompile) {
      options.compilerArgs <<"-Xlint:all,-serial"
    }
  }
}

これにより、アプリをビルドする度にAndroid StudioのMessagesの部分に大量の警告が出るようになりました。まずはこれらをピックアップして改善する習慣をつけるとよいのではないかと考えました。

f:id:sys1yagi:20150915182349p:plain

Lint警告が邪魔

Lint警告の表示を導入した次の振り返りで「ビルドする度に出てくる大量の警告が邪魔」というProblemが出てきました。たしかに警告をもとに改善点を洗い出すのが目的であれば便利ですが、普段の開発においてはノイズです。特にコンパイルエラーとなった時に警告とエラーが混ざってしまい原因が探しづらくなってしまっていました。また期待していたより改善が促進されていませんでした。

Lint警告の表示そのものは有用なので外したくない、しかし普段邪魔になるのでなんとかしたい、他に良い方法はないか。この振り返り時点での細かな技術的負債に対する感情を整頓するとこのように分解できそうです。

  • 普段の開発の負担は増やしたくない
  • 改善はしていきたいがモチベーションがあがらない
  • 単発の大きな改善ではなく小さな改善を継続的にしたい
  • Lint警告は有用だが毎回出るのはイライラするので消し去りたい

これらの感情をうまくカバーする必要がありそうです。

朝Lint活動へ

「朝練...朝れん...朝りん...朝Lint!」

振り返りのさなか私は適当につぶやきました。Lint警告のイライラをバネに朝にパパッと修正する時間を作ったらいいんじゃないかという発想です。はからずもメンバーから「いいのでは」という反応が得られ、早速Tryすることになりました。

朝Lintの方針は次の通りです。

  • 主に軽微なLint警告を片付ける(ビルドのたびに出てくる警告を駆逐していく)
  • レビューの負担を鑑みつつAndroid Lint、Find Bugsなどの警告や、古い実装方針を片付けてもよい
  • 変更は少量でよい(ある警告をいちどに全て片付けなくてもよい)
  • 実施は任意でよい

それまでおこなっていた警告の中から自主的に問題を探しだす方法の場合どうしても修正を加えるに足るほどの問題なのかと考えてしまって手が止まってしまっていました。朝Lint活動という仕組みを導入した事によって修正への心理的ハードルが大きく下がりました。

朝Lint活動の経過

運用し始めておよそ2ヶ月程度が経過しています。それまでに作られた朝LintのPRはおよそ20件ほどです。まだ細々といった感じですが継続的に改善ができています。以前はほぼ0だったので大きな進歩です。

f:id:sys1yagi:20150915182422p:plain

朝Lint活動は細かな技術的負債への対応に対する各種感情をカバーできているように見えます。

  • 普段の開発の負担は増やしたくない
    • 任意、軽微でよいので気が向いた時にやれる
  • 改善はしていきたいがモチベーションがあがらない
    • 軽微な修正でも改善が重なっていく。朝、気分が乗らないときはサクッとPRして弾みをつけるといった使い方もできる
  • 単発の改善ではなく継続的に改善したい
    • 修正中に他の修正箇所を発見するが翌日に持ち越せばよいと思える。
  • Lint警告は有用だが毎回出るのはイライラするので消し去りたい
    • イラッとして修正したくなる。修正してもよい。

朝Lintの中でbugを発見して潰したり、朝Lintの範囲では修正できない問題の検出ができたりなど良い効果もあらわれていて、習慣として定着しそうな手応えを感じています。

おわりに

以上が本体アプリの開発チームが行っている朝Lint活動の背景と内容です。ここに至るまで様々な試みをしてきましたがなかなかうまくワークしていませんでした。今思えば感情をカバーするという点が重要だったのかな、と思います。皆様も朝Lint活動を試してみてください。

複数のクラウドサービス間でオブジェクトストレージの中身を同期する

$
0
0

複数のクラウドサービス間でオブジェクトストレージの中身を同期する

こんにちは。インフラストラクチャー部の加藤(@EugeneK)です。

クックパッドのすべてのレシピやつくれぽ等の画像はAmazon Web Services(以下AWS)のSimple Storage Service(以下S3)にオブジェクトとして保存されています。

S3は99.999999999%の堅牢性と99.99%の可用性を謳っていますが、ユーザさんから預かった大切なデータを守るため、万一に備えて別のクラウドストレージにもバックアップを行っています。

今回はそのオブジェクトを準リアルタイムでGoogle Cloud Storage(以下GCS)に同期されるようにした話をします。

S3にオブジェクトが追加されたことを知る

S3にはEvent Notificationという機能があります。 この機能を用いると、S3のバケット内のオブジェクトが追加・更新・削除されたときに通知イベントを発行することができます。 イベントの発行先は以下の3つから選択できます。

  • Simple Notification Service(SNS)のトピック
  • Simple Queue Service(以下SQS)のキュー
  • Lambda

今回はSQSを発行先として選択しました。

f:id:EugeneKato:20150916182005p:plain

SQSのキューからメッセージを取得する

SQSは名前の通りシンプルなメッセージキューサービスで、Amazon Web Servicesの各種サービスの中でも古くからあるものです。 S3のバケットに対する変更は先の通知イベントにより、SQSのメッセージとしてキューイングされます。 Elastic Compute Cloud(以下EC2)で作成したインスタンスでこのキューからメッセージを1つずつ取得することで、S3バケットに対する変更の内容を知ることができます。

S3の内容をGCSに反映する

EC2のインスタンスはメッセージの中身を読み取って、その情報を元にS3からオブジェクトを読み出してGCSに反映させます。

たったこれだけで「複数のクラウドサービス間でオブジェクトストレージの中身を同期する」ことができるようになりました。

f:id:EugeneKato:20150916182012p:plain

細かい工夫

本筋からは離れますが、便利な機能を使用しているのでここで紹介します。 通常、AWSのAPIを利用するときはアクセスキーIDとシークレットアクセスキーのペアからなるクレデンシャルを用いることが多いですが、EC2のインスタンスにクレデンシャルの情報を持たせずにAPIを利用できるようにしました。

EC2のIAMロールという機能を用いると、ロールを紐付けたインスタンスは、自身のインスタンスメタデータから許可された権限を持つクレデンシャルを取り出すことができます。 ここでは、以下のAPIリクエストを許可するようにしたIAMロールを作成してインスタンスに関連付けました。

  • SQSからのメッセージ取得(ReceiveMessage)
  • 処理が完了したメッセージの削除(DeleteMessage)
  • S3からのオブジェクトの取得(GetObject)

IAMロールを関連付けたインスタンスからAWS-SDKを使用したAPIリクエストを実行すると、AWS-SDKがクレデンシャルの取り出しをやってくれるので、クレデンシャルを意識する必要はなくなります。

過去分のデータを同期する

同期を開始するとそれ以降のデータは同期されますが、同期を開始する以前に既にS3に存在したデータをGCSにコピーする必要があります。 とはいってもわざわざ別の仕組みを用意する必要はなく、同期の仕組みをそのまま使います。

SQSのキューへのメッセージ発行はS3のイベント通知だけが行えるわけではありません。 バケット内にある全てのオブジェクトのリストを作成し、それらをメッセージとしてSQSのキューに発行してしまえば、あとはこれまでに作った仕組みが処理してくれます。 つまり、S3の通知イベントを模したメッセージをSQSに発行してやるだけです。

クラウドらしさを活かしたアーキテクチャ

この仕組みを用いて実際に約20TB(1500万オブジェクト)のデータの同期を行いました。 これだけのデータ量(特にオブジェクト数)となるとかなり長い時間がかかってしまいそうですが、スケールしやすいのがクラウドの強みですのでスケールで解決します。 EC2のインスタンスを複数並べて並列にコピーすることで時間を短縮することができます。

実際に、SQSやS3は同時に複数の処理を行うことが得意なサービスなので、1秒間に数千回もの処理を行っても何の問題もありませんでした。 具体的には50台のインスタンスを使っておよそ200並列で稼働させることで過去分のデータの同期が3日ほどで完了しました。

通常このような並列処理をするとなると、同じ処理が複数回実行されないように分割してやる必要がありますが、この仕組みはSQSを利用しているため必要ありません。 また、SQSには可視性タイムアウトという機能があり、処理途中に何らかの理由で失敗したときも、メッセージの削除を行わずに放置しておけば、しばらく経った後に同じメッセージが再取得できるようになるので結果的にリトライも自動的に行われます。

おわりに

複数のクラウドストレージ間でデータを同期する方法について紹介しました。同期以外にも様々な使い方に応用できると思います。 クラウドサービスが提供する個々のサービスを部品としてつなぎ合わせることで、堅牢で柔軟なシステムを最小限の手間で実現することが可能になります。

やりたいことに集中し、そのために必要となる前提の要素は用意されているコンポーネントを使用する、というのがクラウドサービスの最も良い使い方だと考えています。 ハードウェア資産を持たなくてよいということがクラウドの売りとされがちですが、本当のクラウドの強みはこういったところにあります。

怖くない!エンジニア以外のメンバに気持ちよく GitHub を使い始めてもらうには

$
0
0

ヘルスケア事業部の濱田です。チームで楽しく開発してますか? コードベースの置き場として絶大な支持を集める GitHub。コードを管理するだけでなく、issue を使って様々な議論や報告を行い、その結果をスムーズに製品に反映させることができます。エンジニアだけでなく他の職種のメンバも巻き込んで GitHub で議論ができたら、開発はもっと活発になるでしょう。

一方、 GitHub にはちょっと敷居が高い、敬遠したくなるような雰囲気を感じる人も多いようです。 本記事では、様々な職種のメンバが GitHub を気持ちよく使い始めてもらうにはどうすればよいか、という観点から気をつけるべきことを紹介します。

GitHub は非エンジニアにとっては怖い場所?

エンジニアは GitHub が大好きです。自分たちの作ったコードがあり、ドキュメントがあり、仲間がおり、コードレビューを通じて自分の新たに作った価値が承認される場所です。金曜日の夜に今週のコミット数を眺めてニヤニヤしているエンジニアはきっと多いはずです。

一方、非エンジニアにとってはどうでしょうか。ヘルスケア事業部には多くの職種のメンバが在籍しています(ディレクタ、デザイナ、営業、渉外係、そしてレシピの栄養調整などを担当する管理栄養士等)。議論の場を GitHub へ移そうとした際に尋ねてみたところ、開発に GitHub (Enterprise) が使われていること自体は多くのメンバに知られていましたが、その印象はあまりポジティブなものではありませんでした。

  • そもそもどういうツールなのかわからない
  • エンジニアがいっぱいいる = 専門的
  • 不用意なことをすると怒られそう

印象的だったのは、「GitHub って怖い」という言葉を使った人がいたことです。GitHub が怖い、という言葉の裏には、ツールのとっつきにくさに加えてそこにいるエンジニアが怖いという意味が含まれているんですよね。気持よく使ってもらえるように、いろいろな工夫をして歩み寄っていきましょう。

はじめに時間を取って不安をほぐす

GitHub を使い始めてもらうにあたって、まずはじめに一度説明の時間をとりましょう。 メンバの机のそばに行ってやってもいいですし、人数が複数人いるならミーティングルームで一気にやってしまってもいいでしょう。会話しながらなるべくカジュアルにやるのがポイントです。

まずは issue の使い方だけ覚えてもらう

慣れてしまうと忘れがちですが、GitHub のコンセプトって結構難しいです。 リポジトリがあって、issue が立てられて、pullrequest というコードへの変更の依頼があって、マージして……と、いろんな要素がありますよね。

すべてを説明するには情報量が多すぎるので、まずはコードを直接触らないメンバが一番良く使うであろう issue の機能に絞って説明を行います。このとき、一緒に操作をしながら issue を作る練習をすると良いです。以下のようなポイントを押さえてもらいましょう。

  • issue は掲示板のスレッドや、Facebook の投稿みたいなもの
  • 投稿にはメンバがコメントを付けていける
  • 製品のバグ報告や質問など、トピックごとに issue を立ててほしい
  • 議論がまとまったら、close ボタンを押して issue を一覧から消しておく

書き始めるのを容易に

作り方がわかっても、それだけでたくさんの issue が立ててもらえることはまずありません。どんなときに立てたらいいか、description になにを書けばいいか等、迷うことが多いのが原因のひとつです。気軽に issue を書けるよう、レールを敷いておきましょう。

トピックのパターンを分類し、テンプレート化

issue のトピックには以下のような典型的なパターンがあります。

  • バグ報告
  • 機能要望
  • 議論/質問
  • 実装依頼

パターンごとに必ず書いておいたほうが良いことはある程度決まっている事が多いため、それらを書き出してテンプレートに含めておきます。こうしておくことで抜け漏れも出にくくなり、不備を指摘される可能性も減るため、issue 作成に慣れていない場合でも気が楽です。

issue のタイトルやラベルの付け方等もただ真似すれば済むように、本番を模した issue にしておくのがお勧めです。バグ報告用のテンプレートであれば以下のようになります。

f:id:manemone:20150917162025p:plain

issue の雰囲気を和らげる

エンジニアは職業柄、正確性や客観性を重視した言葉遣いを好む傾向にあります。

認識の食い違い防止や簡潔さのため、GitHub 上でもできるだけそうすべきだと思いますが、時に難しくて近寄りがたい雰囲気が出てしまうのも事実。様々な職種の人が書き込むような issue では、以下のような点に少し気をつけるだけで、グッと発言しやすい場になるはずです。

なるべく早く反応を返す

「ありがとうございます、見ます!」の一言でも良いです。慣れないメンバは、「この聞き方で合ってるかな……?」「見てもらえてるかな?」等、issue 1つ立てるごとにドキドキしていたりします(はじめてネット掲示板に書き込んだ頃のことを思い出してください)。単純なことのようですが、はじめの一言が返ってくるだけで安心できます。

ポジティブなコメントを忘れない

コードレビュー等で記述の不備や疑問点を探すのに慣れているせいか、修正すべき点に言及するのに集中するあまり、良い点を褒めるのを忘れがちです。

わかりやすいディスクリプションを書いてもらえたり、早いレスポンスがもらえたりしたら、ありがとう、いいねという気持ちを積極的に伝えましょう。

絵文字を積極的に使う

文字だけのコミュニケーションではどうしても発言がきつい印象になってしまいがちです。ニュアンスの補足に絵文字を意識的に使っていきましょう。例として、以下に特によく使う絵文字を挙げておきます。

name
f:id:manemone:20150917162604p:plain:thumbsup::clap:よいね、などのニュアンスを加えたいときによく使われます。:thumbsup:だとちょっとカジュアルすぎるかな、というときは :clap:が使われている印象です。
f:id:manemone:20150917162505p:plain:bow:見た目のとおり、申し訳ない気持ちを表現する時に使われます。お辞儀というよりは土下座に近い絵面なのですが、意外と他に代替のきくものがありません。
f:id:manemone:20150917162655p:plain:pray:なにか作業依頼をするとき、お願いします!という意味を込めて使われます。祈りというか、柏手ですね。
f:id:manemone:20150917162614p:plain:ok_hand::ok_woman:「了解」の意味で文の後に置かれます。単体でもよく使われます。

改善された製品を見てもらう

自分発の疑問や提案、報告を元にした変更が製品に反映されるのは嬉しいものです。

直接開発に携わっている実感を持ってもらうために、リリースしたらスクリーンショットを貼ったり URL を伝えたりして、あなたのアクションが改善につながったよ、とアピールしましょう。

文化を共有する - LGTM 画像で楽しく祝福

ある程度 GitHub に慣れてきたメンバには、ソーシャルコーディングならではの文化を少しだけ紹介するのがお勧めです。共有できる文化があると、コミュニケーションの潤滑油となってスムーズに開発が進みます。

例えば、「OK だと思います!」という意味を込めて LGTM (Looks Good To Me) と書き込む習慣がありますが、ヘルスケア事業部ではメンバのスナップ写真などを加工して LGTM 画像を作成し、ここぞというところで issue に貼り付けて楽しく祝福をしています。最近ではエンジニア以外からも「それ LGTM 画像にしよう」のような声も聞こえるようになりました。

f:id:manemone:20150917161724j:plain

一度遠ざかっても、また戻ってこられるように

こうして慣れてくれたメンバでも、ちょっと別の仕事に離れるなどして GitHub から遠ざかると、再び使いはじめる際に高いハードルを感じるそうです。

議論の最中など、良いタイミングがあればすかさずメンションを飛ばしてあげて、「どう思いますか」などと話を振ると自然な流れで戻って来られてよいでしょう。

まとめ

いかがでしょうか。メンバの多様性が高いほど、製品を改善する視点の数も多くなります。なんとなく GitHub 怖いな、で議論に参加しない人が多かったとしたら、それはものすごくもったいないことです。

ヘルスケア事業部では、多くのメンバに GitHub 上での開発に直接加わってもらったことでたくさんの製品改善のアイデアを得ることができました。他にも様々な工夫をして、GitHub を「怖くない、楽しく開発できる場所」にしていきたいと思います。

グループ会社の開発体制強化にデザイナーとして参加した話

$
0
0

こんにちは。ユーザーファースト推進室デザイナーの倉光です。

今年の春、クックパッドのグループ会社として株式会社みんなのウェディングが新たに仲間入りしました!

クックパッド社員が新しいサービスを立ち上げて分社化することもありますが、今回のような場合にはこれまで異なる文化を築いてきた組織がグループに加わることになります。これに伴い、私はこの数ヶ月間に開発組織の新たなサービスデザインプロセスの立ち上げに参加してきました。

今回は自分たちのデザインプロセスを広めるにあたり、どのようなことを実践したかをここで紹介します。

1.まずはお互いのことを知り、課題を把握する

みんなのウェディング社は月間のべ300万人の利用者がいる「みんなのウェディング」という結婚式場の口コミサービスを運営しています。自社内に開発組織を持っていますが、サービス開発体制をより強化したいという課題を抱えていました。そこで開発基盤の再構築、サービスデザインプロセスの見直しといった様々な課題に対し、クックパッドのノウハウの導入も視野に入れ数人のクックパッドスタッフが開発体制強化に参加することが決まりました。

まずお互いを理解するために、私たちは下記のようなことをやってみました。

私たちがやったこと

  • 自分たちについて知ってもらう(2社の開発スタッフが集まったクックパッドでの交流会)
  • 組織を知る(5日間みんなのウェディングへ短期滞在し、開発に参加しながらサービスの空気感を体感する)
  • サービスを知る(市場調査、サービスのウォークスルー、結婚式場の視察参加)
  • 新しい知識習得や手法導入のサポート(開発ツールの見直し、デザインレビュー、UXデザイン手法)

f:id:transit_kix:20150918015123p:plain (交流会の様子と、短期滞在の様子を社内情報共有ツールに載せた記事です)

親会社とグループ会社、クライアントと受注側といった主従関係が発生する関係性の場合、どのタイミングではじめて顔をあわせるかは結構重要なポイントとなります。信頼関係を築く前に仕事が始まってしまった場合、議論をしたくても「上から指示されたから仕方がない」という雰囲気ができてしまえば、あっという間にウォーターフォール構造が出来上がってしまいます。私自身、クライアント側としても受注側としても今まで何度かこのような経験がありました。

そうやっていつの間にか発生してしまう“透明な壁”をつくらないように、初顔合わせは会議室ではなくクックパッドのキッチンラウンジで料理を食べながらというスタイルにしました。この時、より良いサービスに育てるために「ユーザーファーストとは何か」「我々はどういった開発体制を築くべきなのか」といったことを率直な意見も交えながら、徐々に交流を深めていきました。

その数週間後、今度は私が実際にみんなのウェディングに5日間出社し、サービスのデザイン改善業務を現場デザイナー陣と試験的に行ってみました。このときは、私が知らなかったウエディング業界の構造/特徴やプロダクトとしての前提条件などを教えてもらうことができました。そしてこの時、特にみんなのウェディングの強みであると感じたのは「相談デスク」というサービスです。

f:id:transit_kix:20150918015326j:plain

「相談デスク」は結婚式場選びについての相談を専門アドバイザーと対面で相談できるというサービスです。サービスを育てていく上で、ユーザーの「情報をさがす」という行動軸でオンライン/オフラインの両方の選択肢をもち、それらが同組織で交流できるのは非常に強みとなると感じました。

短期間ではありましたが、この事前に実施した滞在調査(エスノグラフィ)が、開発現場の抱えた課題を知る上でとても重要となったような気がしています。

2. 現場に飛び込んで、一緒にデザインする

前準備を経ていよいよ本格始動となります。サービスのコンセプトデザインリニューアルに携わりながら、デザインプロセス自体の見直しに着手しました。約1ヶ月半毎日みんなのウェディングに出社し、現場のプロダクトオーナーやエンジニアとも一緒になって開発をします。ここでは私も画面のワイヤーフレーム制作から、画像素材1点の制作まで行いました。

私たちがやったこと

  • サービスロゴのデザイン変更
  • サイトのコンセプトカラーをピンクからグリーンに変更
  • 情報構造の見直し、画面デザイン、及びデザインレビュー
  • Sassによるデザインフレームワーク化準備(現在進行中)
  • 知見共有できる場づくり(ワークショップ、デザインツールの勉強会、クックパッドとの手がけた事例の知見共有会など)
  • 各種ルールの制定(デザインレビュー指針、ワークフロー)

f:id:transit_kix:20150918020700p:plain (今回デザイナー陣が活用していた資料の一部)

サービスデザインの難しいところは、どんな素晴らしいように見えるアイデアも現実にビジネスとして実現可能なかたちに落とし込む必要があること。現実には収益を改善したい、できるだけ早くリリースしたい、でも技術的負債は抱えたくない、など様々な問題が山積みです。そういった“ややこしいアレコレ”に直面し決断をしていく時こそ、自分たちのサービスに関する信念みたいなものが目の前のプロダクトを通して組織内で浸透していくと思います。

こうしてサイトのリニューアルを8月末に実施し、このタイミングでロゴデザインも新しいものに生まれ変わりました!シンボルはこれまでのチャペル(結婚式場)をモチーフとしていましたが、新郎新婦(ユーザー)をイメージした2羽の鳥をあしらっています。

f:id:transit_kix:20150918021608p:plain

3. プロセスを回しながら、サービスも組織も改善していく

9月から私は再びクックパッドでの業務に戻りました。みんなのウェディングでは、サービス開発本部の皆によって引き続き次の改善施策が進められています。とはいえ、私は今でもみんなのウェディングに週1回は出社し、Slackの開発者の集まるチャンネルに参加し、 GitHub上でのデザインレビューに参加しています。「やっとリモートでも、現場の一人一人とコミュニケーションができるようになってきたな〜」というのが率直な感想でしょうか。

サイトのデザインリニューアルに関しての結果も徐々に集まりつつあります。先日開発者達で施策の振り返りミーティングも行ったのですが、数値が改善したものもあれば、予期せぬ数値が下がってしまい慌てて応急処置をしたものもあり……といった形で、まだまだ今後も長期的な目でサービスを育てていかなければなりません。

そして、当初参加時に目標としていた「デザインプロセスの改善」ですが、

  • ユーザーへのインタビューを、開発者達自身が企画/実施する
  • 機能設計の際には、簡易的にでもまずユーザーのアクティビティ(行動)と、提供するインタラクション(機能)の両面から書いて照らし合わせてみる
  • 開発者同士のデザインレビューを通して、サービス全体の体験の統一を目指す

といったことが徐々に見られるようになっています。

「ユーザビリティエンジニアリング」では、新たな活動が組織に普及・定着するためには段階を踏む必要があり、おおよそ6段階のレベルが考えられるとされています。

f:id:transit_kix:20150918015343p:plain

みんなのウェディングについては、“プロトタイプを使ってユーザーテストを実施し、UX/UCDの専門家が設計チームの要請に応じて“助言者”として随時プロジェクトに参加するフェーズ”に当たる「揺籠期」へと一歩進む段階にあたるでしょう。

まとめ

サービス開発についてデザインプロセスの変化に必要なのは一人一人の「意識の変化」と「行動の変化」の相互作用だと思います。 「意識」は物事に対する姿勢のようなものです。例えば具体的な指針もなく「みなさん、意識を変えてください!」「もっとユーザーファーストになってください!」などと言われてもなかなかピンとこないですよね?そこで「行動」が重要となるわけです。実践してみたことの結果に対し他者からレビューをもらったり内省することで、自分たちの現場でユーザーファーストを実現するための方法論が見えてくると思います。

正解がわからない中で仮説を立て、実行を積み重ね、時には失敗を繰り返しながら、それを外化していく。これが何といっても自社サービスを自分たちで育てていく面白いところだと思います。そしてこれからもまだまだ改善は続きます。

最後にクックパッドみんなのウェディングも、仲間を募集中です!


クックパッドの本番環境で使われている Ruby のバージョンが 2.2 になりました

$
0
0

技術部の鈴木 (@eagletmt) です。

先日、クックパッドで使われている Ruby のバージョンを 2.0.0 から 2.2 にアップグレードしました。 アップグレードは主に @sorahと私で進めました。 今回はアップグレードまでの過程やアップグレード当日の流れ、そして今のところ見られているアップグレードによる効果などについて紹介します。

アップグレードまでの準備

テストを通す

Ruby 2.1 がリリースされたときから 2.1 にアップグレードできないか検証環境でテストを回していました。 しかし、当時はクックパッドの全テストを実行すると必ず途中で Ruby がクラッシュする現象に悩まされていました。 Ruby の GC のバグ、拡張ライブラリのバグを疑いながら色々やってみたものの結局解決できず、Ruby 2.2 がリリースされてからもこの状況は改善されませんでした。

しかしあるとき、たまたま通常の CI と全く同じ環境でテストを実行したところ、いくつかのテストは失敗したものの Ruby がクラッシュすることなく完走しました。 テストが通るようにアプリケーションコードを直しつつ、なぜ通常の CI 環境ではクラッシュせず検証環境ではクラッシュしたのか比較しているうちに、 どうやらマシンスタックのサイズが影響していることがわかりました。 検証環境では Ruby がクラッシュした場合に調査しやすくするため、最適化オプションを切ったりデバッグ用のフラグをつけて Ruby をコンパイルしており、その状態だとクラッシュするようです。 本番や通常の CI では最適化オプションを有効化してコンパイルした Ruby を使っています。 また、RUBY_THREAD_MACHINE_STACK_SIZEを大きめの値に設定すると、検証環境の Ruby 2.2 でもクラッシュせずにテストが完走することが後からわかりました。

長いことクラッシュに悩まされてきたけれども、実は本番で使われている Ruby ではクラッシュしないことがわかってからは、地道にテストの失敗を修正していきました。 といっても REE から 2.0.0 に上げたときのような苦労はなく、2.0.0 でも 2.2 でも動くようなコードに直すことは簡単で、 RUBY_VERSIONrespond_to?で分岐するコードは書かずに済みました。

具体的には以下のような修正を行いました。

  • Time.parse(date, now)の now に Date オブジェクトを渡していた
    • Ruby 2.2 からは now は Time オブジェクトであることが必須になったようなので、Time に変換して渡すようにしました。
  • nil に値を書き込んでいた
    • Ruby 2.2 から true/false/nil が freeze されるようになりました。
    • nil.extend(SomeModule)ということをしていて、SomeModule によって attr_accessorが追加され、そこに値を書き込もうとしてエラーになっていました。
    • これは nil が返るべきではないところで nil が返っていたことで発生していたので、テストコードを修正しました。
  • ハッシュリテラルのキーが重複していた
    • Ruby 2.2 から警告が表示されるようになりました。
    • 依存 gem も含めたクックパッドのコードベース内で、おかしなハッシュリテラルを結構発見することができました。

実際に対応が必要だったのはこの程度だったので、Ruby 2.2 でテストを実行する CI ジョブを設定し、一日に一回実行して確認する程度で十分回っていました。

本番での検証

テストが通るようになってからは、少数の app サーバで Ruby 2.2 にして一時的にサービスインして、エラーやパフォーマンスをチェックしました。 このとき、キャッシュが混ざらないように、dalliの namespace に Ruby バージョンを含めるような工夫をしています。 dalli を使ってキャッシュする場合は Marshal.dumpした値を memcached に保存するため、もし Ruby のバージョンによって Marshal のフォーマットが変わっていた場合、不整合が発生してしまうためです。 また、もし Ruby のアップグレードによる不具合が発生し誤った値がキャッシュに書き込まれてしまった場合に、影響範囲を限定する目的もあります。 Ruby や Rails のバージョンアップのような影響範囲が広くエラーが予測しにくい場合、いつもは数時間ほどサービスインしてエラーとパフォーマンスを確認していますが、 今回は Ruby がクラッシュする懸念があったため、数日間サービスインしたままにしてクラッシュしないことを確認していました。

アップグレード

クックパッドは http://cookpad.comというウェブサイトだけでなく、スマートフォンアプリ向けの API サーバやガラケー向けサイトのモバれぴも提供しています。 この中でウェブサイトと API サーバはとくにサーバ台数が多く影響も大きいため、アップグレード時には app サーバを新規に用意し、そこで Ruby のバージョンを上げ、 リリース時にはロードバランサの設定を切り替える、という方法をとりました。 最初は全体の 50% が Ruby 2.2 になるようにし、その後様子を見ながら 70%、100% と Ruby 2.2 の割合を上げていきました。

他のモバれぴのように比較的サーバ台数が少ないサービスでは、一台ずつ Ruby のバージョンを上げていきました。

今回のアップグレードでは、スタッフ専用のページでエラーが一件発生しただけで、他のエラーやパフォーマンス上の問題などは発生しませんでした。

Ruby 2.2 による効果

まだアップグレードしてからあまり日がたってないので正確なことは言えないのですが、アプリケーションの応答速度が改善しました。 クックパッドではアプリケーションのパフォーマンスの指標の一つとして X-Runtimeを記録しています。 その一日の平均値を比較するとおよそ 5% から 10% ほど改善していました。

アプリケーションコードを書く上で便利なものとしては、キーワード引数のデフォルト値を省略できるようになったことが大きそうです。 キーワード引数の文法は 2.0.0 で追加されましたが、2.0.0 では常にデフォルト値を指定する必要があり、必須のパラメータを渡すときには使えませんでした。

Ruby 2.1 以降はデバッグやプロファイリング関連の機能が強化されています。 それを利用した gem が既にいくつもあるので、それらを利用することでアプリケーションのパフォーマンス改善をしやすくなるのではないかと思います。

まとめ

クックパッドの本番環境で Ruby 2.2 が使われるようになるまでの過程について紹介しました。 最新の Ruby が一番いい Ruby なので、できるだけ最新の Ruby を使うようにして開発者とユーザ双方がより幸せになるようにしていきたいです。

Elasticsearch のインデックスを無停止で再構築する

$
0
0

こんにちは。ホリデー株式会社の内藤です。

ホリデー株式会社では Holiday(https://haveagood.holiday) という新規サービスの開発・運営を行っています。*1

以前投稿した記事でご紹介したように、Holiday では全文検索エンジンとして Elasticsearch を利用しています。

Ruby on Rails で構築されたアプリケーションから Elasticsearch を操作するには、公式 gem である elasticsearch-railsを使うのがとても便利です。 もちろん、Holiday でも活用させてもらっています。

大方の機能についてはこの gem で提供されるもので満足だったのですが、一点だけ、Holiday の運用をしている中で困ることがありました。 それが、サービス公開後のインデックスの再構築です。

elasticsearch-rails gem には、データのインポート用の Rake Taskが既に用意されています。 使い方は非常に簡単で、下記のようにタスクファイルを作成しrequire文を一行加えるだけで、

# lib/tasks/elasticsearch.rakerequire'elasticsearch/rails/tasks/import'

マッピングの再構築およびデータのインポートを行う処理を呼び出すことができます。

$ bundle exec rake environment elasticsearch:import:model CLASS='Spot'FORCE=y

しかし、このタスクを実行すると既存のインデックスが上書きされてしまい、まっさらな状態に一度初期化されてから、マッピング定義やデータのインポートが行われることになります。 つまり、この間は適切な検索結果を返すことができなくなるため、サービスを停止せざるを得ないという状況になってしまいます。

サービスを運用していると、「マッピング定義を変更したい」「アナライザーの定義を見直してインデックスを作りなおしたい」ということが度々起きます。 その度にメンテナンス画面を掲出するとなると、継続的にユーザにサービスを提供することができなくなってしまいます。

そこで、サービスを停めること無くインデックスを作り直すためにはどうすればよいのかについて考える必要があります。

本稿では、elasticsearch-rails gem を使う前提で、前述の問題を解決する方法を実装例を交えて紹介しようと思います。

基本的な考え方

無停止でのインデックス再構築を行うためのアイデアは、Elasticsearch の公式ブログで紹介されています。 Changing Mapping with Zero Downtime

この記事によると、Index Aliasesの仕組みを利用することでこれを実現できるそうです。

Elasticsearch では、インデックスに対して、エイリアス(別名)をつけることができます。 例えば、spots-v1というインデックスに対して、spotsというエイリアスを付与した場合、spotsに対して行った操作は、実際には spots-v1に対して行われるようになります。

f:id:qtoon:20150925151056p:plain

このようにしておけば、新しいマッピング定義でインデックスを作り直す場合には、裏側で spots-v2を作っておき、準備完了後に spots-v2spotsエイリアスを貼り替えることで、サービスを停止することなくインデックスの再構築ができるというわけです。

f:id:qtoon:20150925151144p:plain

では、上記の仕組みを実装に落とし込んでみます。

※ 本稿で紹介するサンプルコードは、GitHub 上でも公開しています。 https://github.com/9toon/es-reindexing-sample

ここからの例で使う Spotモデルの基本的な設定は以下の通りです。

# app/models/spot.rbclassSpot< ActiveRecord::BaseincludeElasticsearch::ModelincludeElasticsearch::Model::Callbacks

  index_name "#{Rails.env}-#{Rails.application.class.to_s.downcase}-#{self.name.downcase}"

  mapping do
    indexes :id, type: 'string', index: 'not_analyzed'
    indexes :spot_name, type: 'string', analyzer: 'kuromoji'
    indexes :address, type: 'string', analyzer: 'kuromoji'
    indexes :location, type: 'geo_point'end

  settings index: {
    number_of_shards: 1,
    number_of_replicas: 0,
  }

  defas_indexed_json(options = {})
    { 'id'        => id,
      'spot_name' => name,
      'address'   => address,
      'location'  => "#{lat},#{lon}",
    }
  endend

index_nameは参照するインデックス名を指定するのに用います。 実際には、上記で説明したように同名のエイリアスを作成し、それを参照することになります。

インデックスの作成

まずは、インデックスを作成する処理を見てみます。

新しいインデックスを作成する Rake Task は以下のようになります。

# lib/tasks/elasticsearch.rake
namespace :elasticsearchdo
  namespace :indexdo
    desc "Create a new index. Specify IMPORT=1 for rebuilding from resource"
    task create: :environmentdo
      new_index_name = "#{Spot.index_name}_#{Time.now.strftime("%Y%m%d_%H%M%S")}"

      puts "========== create #{new_index_name} =========="Spot.create_index!(name: new_index_name)

      ifENV['IMPORT'].to_i.nonzero?
        puts "========== import #{new_index_name} from data sources =========="

        batch_size = ENV['BATCH_SIZE'] || 1000Spot.__elasticsearch__.import(index: new_index_name, type: Spot.document_type, batch_size: batch_size)
      endendendend

インデックス名については、new_index_name = "#{Spot.index_name}_#{Time.now.strftime("%Y%m%d_%H%M%S")}"としているように、モデル内で指定した index_nameの末尾に日時を加えています。 こうすることで、一意にインデックス名を定めることができますし、いくつかの世代のインデックスがあった場合に、どちらがより新しいのかが分かりやすくなるという効果もあります。

上記タスクに含まれる Spot.create_index!の中身は以下の通りです。

# app/models/spot.rbclassSpot< ActiveRecord::Base

  ...

  class<< selfdefcreate_index!(name: )
      client = __elasticsearch__.client

      client.indices.create(
        index: name,
        body: { settings: self.settings.to_hash, mappings: self.mappings.to_hash }
      )
    endendend

では、このタスクを実行します。

$ bundle exec rake environment elasticsearch:index:create IMPORT=1========== create development-esreindexingsample::application-spot_20150924_141353 ==================== import development-esreindexingsample::application-spot_20150924_141353 from data sources ==========[INDEX][Spot] Created: development-esreindexingsample::application-spot_20150924_141353

インデックスが正常に作成されたか確かめてみます。

# GET http://localhost:9200/development-esreindexingsample::application-spot_20150924_141353?pretty=1

{
  "development-esreindexingsample::application-spot_20150924_141353" : {
    "aliases" : { },
    "mappings" : {
      "spot" : {
        "properties" : {
          "address" : {
            "type" : "string",
            "analyzer" : "kuromoji"
          },
          "id" : {
            "type" : "string",
            "index" : "not_analyzed"
          },
          "location" : {
            "type" : "geo_point"
          },
          "spot_name" : {
            "type" : "string",
            "analyzer" : "kuromoji"
          }
        }
      }
    },
    "settings" : {
      "index" : {
        "creation_date" : "1443071633270",
        "number_of_shards" : "1",
        "number_of_replicas" : "0",
        "version" : {
          "created" : "1070199"
        },
        "uuid" : "TCiNzSnuRIqsj1ZIwM1iOg"
      }
    },
    "warmers" : { }
  }
}

このように、development-esreindexingsample::application-spot_20150924_141353という名前で、正常に新しいインデックスが作られたことが確認できました。

エイリアスの貼り替え

あとは、このインデックスに対してエイリアスを付与することで、新旧のインデックスを切り替えられるようにします。 エイリアスを切り替えるタスクは以下の通りです。

# lib/tasks/elasticsearch.rake
namespace :elasticsearchdo
  namespace :aliasdo
    task switch: :environmentdoraise"INDEX should be given"unlessENV['INDEX']
      new_index_name = ENV['INDEX']

      puts "========== put an alias named #{Spot.index_name} to #{new_index_name} =========="Spot.switch_alias!(alias_name: Spot.index_name, new_index: new_index_name)
    endendend

Spot.switch_alias!の中身は次のようになっています。

# app/models/spot.rbclassSpot< ActiveRecord::Base

  ...

  class<< selfdefswitch_alias!(alias_name: , new_index: )
      client = __elasticsearch__.client

      old_indexes = client.indices.get_alias(index: alias_name).keys

      actions = []
      actions << { add: { index: new_index, alias: alias_name } }
      old_indexes.each do |old_index|
        actions << { remove: { index: old_index, alias: alias_name } }
      end

      client.indices.update_aliases(body: { actions: actions })
    endendend

このメソッドは、先ほど作成したインデックスに対してエイリアスを付与し、古くなったインデックスからエイリアスを除去する役割を担います。

このメソッドの内部では、update_aliasesメソッドが呼ばれます。 このメソッドによって、エイリアスの追加・削除を一回のリクエストで同時に行うことができます。

ではこのタスクを呼び出してみましょう。

$ bundle exec rake environment elasticsearch:alias:switch INDEX='development-esreindexingsample::application-spot_20150924_141353'========== put an alias named development-esreindexingsample::application-spot to development-esreindexingsample::application-spot_20150924_141353 ==========

エイリアスの貼り替えが正常に行われたかを確認します。

# GET http://localhost:9200/development-esreindexingsample::application-spot?pretty=1

{
  "development-esreindexingsample::application-spot_20150924_141353" : {
    "aliases" : {
      "development-esreindexingsample::application-spot" : { }
    },
    "mappings" : {
      "spot" : {
        "properties" : {
          "address" : {
            "type" : "string",
            "analyzer" : "kuromoji"
          },
          "id" : {
            "type" : "string",
            "index" : "not_analyzed"
          },
          "location" : {
            "type" : "geo_point"
          },
          "spot_name" : {
            "type" : "string",
            "analyzer" : "kuromoji"
          }
        }
      }
    },
    "settings" : {
      "index" : {
        "creation_date" : "1443071633270",
        "number_of_shards" : "1",
        "number_of_replicas" : "0",
        "version" : {
          "created" : "1070199"
        },
        "uuid" : "TCiNzSnuRIqsj1ZIwM1iOg"
      }
    },
    "warmers" : { }
  }
}

このように、先ほど作成したインデックスに対して、エイリアスが貼られているのを確認できました。

まとめ

ここまでで、無停止でのインデックス再構築を行える環境が整いました。

マッピングの再定義やアナライザーの設定変更を無停止で行えるようになると、検索改善の施策を気軽に試すことができるようになります。 新しく作った定義がちょっと違うなーとなれば、ひとつ古いインデックスにエイリアスを貼り替えることで、即座にロールバックすることも可能です。

プロダクション環境で Elasticsearch を使う際には、インデックスを直に指定するのではなく、エイリアスを使って指定するようにしておけば、サービスの運用が非常にやりやすくなるのでオススメです。

本稿が、Rails と Elasticsearch を使っている方々にとって少しでも参考になっていれば幸いです。

なお Holiday では、日本の休日をもっと楽しくしたいエンジニアを募集しています。 もしご興味をお持ちいただけた方がいらっしゃいましたら、ぜひぜひご応募ください!

ご応募はこちらから ↓

*1:元々はホリデー事業室というクックパッド社内の一部署という建て付けで活動していましたが、今年の4月からクックパッドの完全子会社として分割されました

広告ブロッカーの検知と計測について

$
0
0

こんにちは広告事業部の芳賀(@func09)です。

iOS9からの新機能である Content Blocking Safari Extensionsを利用して広告の表示をブロックするアプリがリリースされて、ネットでも結構話題になっていました。

広告コンテンツをブロックするツールということで、普及の仕方によってはメディアの収益に影響を与えうるものです。実際に広告ブロッカーアプリをインストールして、Safariを利用すると 一部の広告はブロックされるようになりました。現時点では英語圏のネットワーク広告などは消えるが、日本語圏の広告にはまだ対応されていないことが多いようです。

収益にどのくらい影響を与えるのか?ということを調査するにも、まずどのくらいの利用者が広告ブロッカーを使っていて、どのくらいのインプレッションに影響があるのかを、定量的に計測することが必要だと考え、その仕組みを導入しています。

広告ブロッカーの仕組み

広告ブロッカーを検知する仕組みを作るには、まず Content Blocking Safari Extensionsでできることを理解しておく必要があります。

Content Blocking Safari Extensionsは以下のようなJSONで定義したルール群を持ちます。

[
    {
        "action": {
            "type": "block"
        },
        "trigger": {
            "url-filter": "webkit.org/images/icon-gold.png"
        }
    },
    {
        "action": {
            "selector": "a[href^=\"http://nightly.webkit.org/\"]",
            "type": "css-display-none"
        },
        "trigger": {
            "url-filter": ".*"
        }
    }
]

ルールは、triggeractionに分かれていて、コンテンツが triggerに指定されている条件にマッチすれば、actionを実行します。

triggerurl-filterresource-typeなど URLやファイル名、リソースのタイプでフックすることができます。

evil_ads.jsというファイルをロードしないようにするには以下のようになります。

[
    {
        "action": {
            "type": "block"
        },
        "trigger": {
          "url-filter": "evil_ads.js"
        }
    }
]

.evil_ads_boxというDOMを表示したくなければ

[
    {
        "action": {
            "type": "css-display-none",
            "selector": ".evil_ads_box"
        },
        "trigger": {
          "url-filter": ".*"
        }
    }
]

こんな感じになります。

実際のコードは OSSになっている BlockPartyの実装が参考になります。

広告ブロッカー検知の方法

広告ブロッカーを検知するためには、上記のような挙動のどれかひとつを検知できれば良いので、一番簡単な「特定の要素を非表示にする」挙動を検知するようにしています。

OSSでは BlockAdBlockというPCブラウザ用の広告ブロッカーアドオンを含めて検知できる便利なライブラリがあったのですが、ごく単純な実装にするために参考するにとどめました。

<divid='ad_blocker_bait'class = 'pub_300x250 pub_300x250m pub_728x90 text-ad textAd text_ad text_ads text-ads text-ad-links'></div><scripttype='text/javascript'>function checkBait(){var bait = $('#ad_blocker_bait');return bait.css('display') === 'none' || bait.css('visibility') === 'hidden';}  setTimeout(function(){if(checkBait()){// 広告ブロッカーが有効になっている}else{// 広告ブロッカーが有効になっていない}}, 1000);</script>

非常に簡単なスクリプトですが、餌となるセレクターを持った空の要素を作っておき、1秒後に消えていないかチェックするだけです。

検知ロジックについて

このロジックは、この記事を書いている2015年9月28日の時点で、主要な広告ブロッカーアプリに対して有効であるとは考えていますが、今後も有効であるかは保証しかねます。

非常に簡単なものなので、広告ブロッカーの精度に合わせて改変していく必要があると思います。

計測について

計測すべきか否かという点については、仮に他社の利用率などが公表されたとしても、メディアによってユーザー特性は変わるため、当然インストールしているアプリも違うはずなので、きちんと自社で計測していくのが良いと考えています。

何をどのくらい測るべきか?もメディア次第なのですが、広告視点で考えて在庫に対してどのくらいの割合が広告ブロックされたものなのか?それによる収益への影響はいくらなのか?ということがわかるように、特定のページにおいてサンプリングした上で利用・非利用の数をとっています。

まとめ

今回は、Content Blocking Safari Extensionsを利用した広告ブロッカーについてとその検知の仕方について書いてみました。対応の仕方はメディアによって様々ではありますが、定性的な話題や周囲の対応に流されず冷静な判断をするために、まずは計測をして、収益インパクトを推測してみるといいのではないでしょうか。

最後に、広告事業部ではメンバーを大募集しております。とりあえず話だけでも・・という方でも良いので是非気軽にご連絡ください!

資源効率の悪いモバイルアプリのリリースを防ぐための資源監視

$
0
0

Android/iOSアプリを開発している皆様、こんにちは。技術部の松尾(@Kazu_cocoa)です。テストエンジニアとして、サービスの品質を向上するために様々な活動を行っています。特に最近はモバイルアプリに注力しています。

この記事をご覧になっている皆さんは、モバイルアプリに対する品質をどのようにお考えでしょう?例えば、アプリがクラッシュしないとか、アプリが機能不全無くシナリオを実施できるとか、そういう面は想像が容易だと思います。品質に対する機能的な側面の指標の1つですね。

品質を考える上では機能的な側面だけではなく、非機能的な側面も考える必要があります。例えば、モバイルアプリを使っているときにサクサク動いているとか、そういう観点は利用時の効率性という側面を持ちます。これにはCPU使用率やメモリ使用量、通信量、見せかけのUIなどが関係してきます。これらの指標を常時取得、監視する、ということは(私が聞く限りでは)あまりされていないのではないでしょうか。(組み込み開発であれば、常に確認しているであろうことですが。)

この記事では、私たちが経験した失敗と、その学びのために行い始めたモバイルアプリの資源監視に焦点を当てて話をします。

Androidにおける資源監視の話

失敗の経験

弊社では過去にCPUを使いすぎる問題を埋め込んだAndroidアプリを世にリリースしてしまいました。比較的新しい端末における短時間の利用ではあまり気づかないですが、古い端末にて長時間アプリを利用していると明らかにアプリの動作が緩慢になる類のものでした。

使っているうちにクラッシュするわけではないので、Fabric/Crashlyticsなんかでは即座に気づくこともできませんでした。しかし、弊社に送られてくるご意見やレビューを観察していると『動きが遅くなった』というような書き込み件数が増加している傾向にありました。分析を進めるなかで、CPU使用率などの資源利用量が不具合が発生したバージョンを境に増加していることを知りました。

無事、不具合は修正したのですが、弊社のリリースフローの中でちゃんと資源監視を行っていれば少なからず世に出ることを防げた問題でした。これは、私たちがアプリをリリースするまでに、機能的な要素は毎回確認しつつも、非機能的な要素は時折しか確認できていなかったことが引き起こした問題でもありました。

KPTによるTryの見定め

この問題をKPTの振り返り時にどのように防止できるか考えました。(弊社では、「クックパッドモバイルアプリの開発体制とリリースフロー」にある通り、定期的にKPTとして振り返りを実施しています)

解決したい問題は アプリをリリースするまでに意図しない資源の異常利用を検出するです。理想としては、リリース物そのものに対して、そのような数値を計測し続けることです。(Proguardやlog送信などの設定がリリース物同様であることが重要であるため。)

Android Studio1.3からはCPUの資源監視が容易にできるようになりました。ただ、この機能は開発時やこのような不具合を見つけようと使っている時にデバッグの一部として発見できる可能性は高くなります。しかし、常にリリース物に対して数値を観察できるわけではありません。

資源監視を行う手段として adbコマンドの sysinfoを使うことにしました。このコマンドはAndroid2.3などの時代から、システムの情報を取得するために利用していた方も多いかもしれません。(ツールの話は後述)

リリースごとの資源監視

現在のクックパッドのAndroid/iOSのリリースでは、Appiumを使った自動化されたテストを複数シナリオ実施しています。テスト対象としてのアプリは、Androidは特にリリース物をそのまま動かしています。この自動化された仕組みに資源監視を組み込むことで、リリースまでの間に必ず資源監視ができるようになります。これにより、今後はバージョンごとに常に同じシナリオに対して監視が可能なので過去を比較にしながら異常の検出が可能になりました。

人手による実施だと操作に対する時間の点で多少の揺らぎが発生してしまいますが、それの考慮はほとんど必要ありません。自動化された環境だから可能な方法ですね。

資源監視の成果

ここ5ヶ月くらいの間、前述した方法にて資源監視を行い続けていました。その間、意図しない資源喰いがないことを確認しながらリリースを繰り返すことができるようになりました。もちろん、その期間の中で資源喰いの問題が改善してることも確認できました。

資源監視のためのツール

droid-monitor

以下に、資源監視として使っているライブラリを公開しています。

https://github.com/KazuCocoa/droid-monitor

やっていることは非常に簡単で、Rubyスクリプトで adbコマンドで得た情報を整形し、Googleの提供するグラフ描画のChartsを使いグラフにしているだけです。もちろん、serial指定することで1台の計算機に複数のAndroid端末を接続していても、それぞれの端末に含まれるアプリの資源を計測可能です。実質、構想から動作させるまでに2人日とかかっていないため、費用対効果という意味でも必要十分なものでした。

※Android 6.0対応対応など、まだ欠けているところもあります

droid-monitorによる資源監視例

droid-monitorでは、CPU、memory、network、gfxinfoの4つの情報をサポートしています。このライブラリを実施しながらテストシナリオを実行することで、リリースごとの資源監視として指標を取得しています。

以下ではそのうち幾つかのグラフを例としてのせます。(すべて、GitHubのREADMEに添付しているサンプルです。)

CPU

memory

Android OS 4.2を境に、取得できる形式が変わっています。それの境目は、接続端末のAPIバージョンを読んで自動で識別します。以下画像はAndroid OS4.2のものです。

Net

ネットワークのsend / receiveの双方を、別々のグラフとして出力します。

iOSの資源監視の話

iOSでも同様に探してみましたが、iOSでは adbほど気軽に計測する手段がありません。そのため、前述した adbほど現実的に計測するスクリプトは作れていません。

Instruments経由で計測はできますが、Appiumを実行する時にinstrumentsも利用するため現状は使うことができません。シミュレータ上でAppiumを実行する時に特定プロセスのCPU使用率など監視できそうですが、現在はそこまで手が行き届いていません。

締め

AndroidアプリやiOSアプリの資源監視は、Webアプリの監視ほど日常的に行われるものではありません。一方で、資源を使いすぎる問題は開発時には意図しないところで湧き出てきたりします。それは利用時の品質を大きく損ねる可能性が高いものです。この記事では、弊社の実際の失敗とその改善話を交えながら、その問題に対する改善話を載せました。

最後に

サービスの品質にはモバイル/Web問わず、様々なSoftware Qualityに関する知見が必要です。Software Qualityが関わる領域は既に様々な分野の知見を蓄積しています。一方、 サービスの品質として一様な指標はまだまだです。私たちは、今回のような機能的/非機能的、はたまた組織的な側面から、様々な知見を持ってより良い品質のサービスを創造するための志を持った仲間を募集しています。

クックパッド テストエンジニアの募集

Wantedlyのページクックパッド株式会社 採用情報

機械学習によるレシピの自動分類、その裏側

$
0
0

こんにちは。検索編成部&研究開発チームの原島です。

クックパッドのレシピには、内部で、様々な情報が付与されています。例えば、こちらの「母直伝♪うちの茹でない塩豚」というレシピには「肉料理」という情報が付与されています。これらの情報は、クックパッドの様々なプロダクトで利用されています。

レシピに情報を付与する方法は沢山ありますが、その一つに機械学習があります。クックパッドでは、レシピが肉料理か否か、魚料理か否か、...という分類を行うことで、「肉料理」や「魚料理」などの情報をレシピに付与しています。

今日は、分類をどのように実現しているか、その裏側を紹介します。

■ 実装フェーズ

まず、分類器を実装する際に気をつけたことを紹介します。

モデルを決定する

分類を行うには、そのための機械学習のモデルを決定する必要があります。クックパッドでは、十分な精度が出るだけでなく、リファレンスが多いという点も考慮して、SVM を採用しています。実装には liblinearを利用しています。

また、冒頭で説明した通り、分類は二値(e.g., 肉料理か否か)にしています。多値(e.g., 肉料理か魚料理か...)にすると、各カテゴリに対する分類が独立でなくなって、結果を改善するのが難しくなるためです。

訓練データを用意する

SVM の学習を行うには、そのための訓練データが必要です。例えば、肉料理の分類器を構築するには、肉料理のレシピ(正例)と肉料理でないレシピ(負例)が必要です。

訓練データを用意する方法はいくつかありますが、コストがかかるのが悩みのタネです。訓練データは正しくないといけません。そのため、これを用意するには、どうしても人手が必要です。

そこで、クックパッドでは、レシピカテゴリを利用して、訓練データを用意しています。レシピカテゴリには、自薦 or 他薦によって、各カテゴリのレシピが蓄積されています(下図)。

f:id:jharashima:20150930162845j:plain

肉料理の訓練データを用意する場合、「お肉のおかず」というカテゴリを正例として、その他のカテゴリを負例として利用しています。人手を介したデータを利用することで、訓練データを用意する手間を大きく削減しています。

テストデータを用意する

分類精度をチェックするには、テストデータが必要です。精度をチェックする仕組みがないと、デグレが起こった時に気付けません。これをサボるとひどい目に遭います(遭いました)。テストデータは必ず用意しておくべきです。

クックパッドでは、分類器を更新する際、テストデータにおける精度をチェックしています。そして、精度が閾値を下回った場合、分類器を更新しないようにしています。こうすることで、デグレが起こっても、本番環境に反映されないようにしています。

開発データを用意する

分類器を構築した後、その精度を改善するには、チューニングが必要です。そして、精度が分かれば、その値に基づいて、分類器をチューニングできそうです。

テストデータがあれば、精度が分かります。しかし、テストデータを用いてチューニングしてはいけません。単にテストデータにおける精度が良くなるだけで、その他のデータにおける精度が良くなっているとは限りません。

そこで、テストデータとは別に、開発データも用意しています。開発データを用いてエラーを分析し、分類器をチューニングすることで、分類器が汎用的になるように気をつけています。

■ 改善フェーズ

さて、モデルを決定して、データも用意しました。とりあえず実装できます。しかし、とりあえずで実装した分類器の精度は、大抵悲惨なものです。分類器を改善する必要があります。

以下では、分類器を改善する際に気をつけたことを紹介します。

素性を修正する

最初に思いつくのが、この方法ではないでしょうか。必要な素性を追加したり、不要な素性を削除すれば、精度が良くなりそうです。

しかし、本タスクでは、素性を修正することはほとんどありません。と言うのは、以下で紹介する方法の方が簡単で、効果が高かったからです。

訓練データを修正する

本タスクで一番効果が高かったのは、訓練データを修正する方法です。正確には、「訓練データに利用するレシピを変更する」です。訓練データにはレシピカテゴリを利用しています。利用するカテゴリを変更すれば、分類器の振る舞いも変更できます。

素性を修正するには、少なからずコードを変更する必要があります。特に、素性を追加する場合(レシピから新しい素性を抽出する場合)は、そのためのコードを追加で実装しなければいけません。

一方、我々の場合、訓練データを修正するだけであれば、ほとんどコードを変更する必要がありません。利用するカテゴリを変更するだけで、実装的には定数を変えるだけです。クックパッドでは、訓練データを修正することで、分類器を改善することが多いです。

ルールで直す

ここから先は力技です。

困ったことに、素性を修正しても、訓練データを修正しても、分類誤りを直せないことがあります。このような場合、汎用的なルールが作れれば、そのルールで対応しています。

例えば、サラダの分類器を構築する時、タイトルが「サラダ」で終わるレシピはサラダとみなしても良さそうということが分かりました。このような場合、機械学習に拘らず、ルールを優先して分類を行うようにしています。

手で直す

素性を修正しても、訓練データを修正しても、直りません。汎用的なルールも作れません。それでいて、ユーザさんからお問い合わせがあるなど、何が何でも直さないといけない時があります。

こんな時は、手で直しています。こういう事態を想定して、スタッフが結果を修正するための社内ツールを用意しておくと良いです。ここでも機械学習には拘りません。堂々と手で直しています。

ちなみに、手で直したデータは、後で訓練データとして利用できます。地道な手作業も、積もり積もれば、分類器を賢くしていくのです。

■ まとめ

本エントリでは、レシピの自動分類の裏側を紹介しました。まとめると、

  • 着実な方法で実装する(実績のあるモデルを使う、地道にデータを用意する)
  • 手軽な方法で改善する(素性でなく、訓練データを修正する)
  • 最後は機械学習に拘らない(ルールで直す、手で直す)

といったところでしょうか。

ネットを漁っても、プロダクトに機械学習を導入した時の知見はそれほど多くないようです。本エントリが、私と同じく、機械学習に悩める方の一助となれば幸いです。

なお、検索編成部では検索エンジニアを、研究開発チームではレシピ翻訳エンジニア画像解析エンジニアを募集しています。ご興味がある方は、是非ご応募ください。

Viewing all 726 articles
Browse latest View live