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

年間100冊以上本を読むための読書術とおすすめ書籍

$
0
0

こんにちは。会員事業部の小久保です。

有料会員のプロモーション、及び他社企業とのアライアンスを担当しており、直近ではユーザーの入退会の傾向に関するデータ分析をしています。

ちょうど1年前くらいからディレクターとして働くことになったのですが、当時はまだディレクション業務を経験したことがなかったため、必要な知識をどのように身に付ければよいか悩んでいました。

そこで、会社の先輩に相談したところ、 「”本をたくさん読む”、”経験を積む”の2つがあると思うけど、”経験を積む”には自分ひとりではすぐに取り組むことができないから、まずは本を読んでみたら?」 とアドバイスをもらい、本を読むことから始めることにしました。

そして、継続して読書を続けるために先輩との約束事として、読書をする上でのルールを決めました。 今回は、そのときに決めたルールを『継続して本を読むための読書術』としてご紹介します。

1.読んだ本に関する要約と感想を人に報告する
この”人に報告をする”ということが継続の大事なポイントになります。少しさぼりたくなった時でも、自分以外の人を巻き込むことで、自分自身へプレッシャーをかけることができます。さらに、要約や感想を書くことで本についての理解もさらに深まります。

私の場合は、一緒にルールを決めてくれた先輩に、毎週月曜日に、要約と感想を報告していて、報告後にもらえる一言コメントも自分のモチベーションに繋がっています。

2.読む冊数を決める
まずは週1冊から始め、徐々に数を増やしていき一番多い時には週6冊の本を読んでいました。しかし、週6冊読んでいると自分の生活リズムが崩れ始めたため、無理なく続けられる冊数まで減らし、現在は週3冊を読むようにしています。

3.読む本のジャンルを決めておく
読む本の冊数が安定したら、1週間に読む本のジャンルが偏らないように注意しました。今は、下記の3つから選ぶようにしています。

  • 業務に関係がある本

  • 自分が興味のある本

  • 分厚くて読むのを後回しにしたくなるような本(ただし1章のみ)

こうすることでバランスよく様々なジャンルの本を読むことができ、多角的な視点が身につけられます。また、飽きずに楽しんで読書を続けることもできます。

分厚い本は避けがちですが、週に1章と決めることで無理なく読みきれるようになりました。

このルールに従って本を読んだ結果、年間で100冊以上読むことができました。平日にも読書の時間を確保していますが、読書をするようになってから土日の過ごし方はかなり変わったと実感しています。本を読むのは苦手だな、という方の参考になれば幸いです。

さて、次からは、これまで読んできた本の中で特に印象に残ったものを何冊かご紹介します。

仕事に役立った
・14のフレームワークで考えるデータ分析の教科書(著書:高橋威知郎)
この本は、はじめてデータ分析をして失敗が続いていた時に見つけた1冊です。データ分析に関しておすすめされている書籍をいくつか読みましたが、自分が体験した失敗の原因と解決方法について具体的に書かれているので、今でもよく繰り返し読んでいます。

14のフレームワークで考えるデータ分析の教科書

14のフレームワークで考えるデータ分析の教科書

また、同じ著者が書いている「ロジカルデータ分析」もおすすめです。「データ分析の教科書」は実際に分析をするときの手法が書かれているのですが、「ロジカルデータ分析」は収益をあげるためのデータ分析とはどういうものかということが広い視野で書かれています。自分のデータ分析が会社で本当に活用されているのか、どこに問題があるのかを気づく指標として参考になりました。

勉強になった
・影響力の武器(著書:ロバート・B・チャルディーニ)
この本は同じ部署でプロモーションを担当している先輩に勧めてもらいました。人がどういうときに心を動かされるのかについて、具体的な事例を用いて書かれている社会心理学の本です。実際に企業が実施をしている様々な戦略が書かれているのですが、顧客目線で読んだ時に、自分も騙されていたなと実感をする事例が多くありました。

影響力の武器[第三版]: なぜ、人は動かされるのか

影響力の武器[第三版]: なぜ、人は動かされるのか

感動した
・海賊と呼ばれた男(著書:百田尚樹)
2013年に本屋大賞を受賞しており、2016年今年の冬には映画化もされるのでご存知の方も多いかと思います。主人公の意思決定の仕方や持っているビジョンを曲げない姿勢がとても印象的でした。出光興産創業者をモデルとし、主人公の一生と会社が大企業になるまでの過程が描かれています。

海賊とよばれた男(上) (講談社文庫)

海賊とよばれた男(上) (講談社文庫)

食文化について
・にほんの食ごよみ(著書:橋本加名子)
この本はクックパッドのライブラリースペースに所蔵されていて見つけた1冊です。昔ながらの日本の行事と12ヶ月分の季節のレシピが紹介されています。この本を読んで日本の食文化を継承することの重要性を感じ、今年のお正月にはじめておせちを作りました。今後、季節のレシピ特集を組む機会があれば参考にしようと思っています。

昔ながらの行事と手仕事をたのしむ、十二か月のレシピ にほんの食ごよみ

昔ながらの行事と手仕事をたのしむ、十二か月のレシピ にほんの食ごよみ

・30分で一生使えるワイン術(著者:葉山考太郎)
お酒を飲む人には、この本もおすすめです。ワインを選ぶときにどういう基準で選べばよいのか分からなかったのですが、最低限のワインに関する知識がとても分かりやすく書かれているため、自分が飲みたいワインを意識して選べるようになりました。お酒関係の企業で働いている友達もこれは分かりやすい!とお墨付きをもらいました。

(073)30分で一生使えるワイン術 (ポプラ新書)

(073)30分で一生使えるワイン術 (ポプラ新書)


これまでに読んだ書籍の一覧
1年前から読み続けてきた書籍の一覧です。業務に関係ないものも一部混ざっていますが何かの参考になればと思います。(読んだ順で並んでおります)


まとめ
ディレクターとして必要な知識を身に付けるために始めた読書ですが、継続して続けたことで、

  • 業務に関する知識が増え、業務を円滑に進めることができるようになった

  • 物事を論理的に説明できるようになった

  • 自分への自信になった

など、さまざまな変化を起こすことができました。また、会社や友達の間でも読書ブームが起き始めています。

いろんなジャンルの本を読むことで、一見業務に関係がなさそうな本であっても、ものごとの考え方や表現の方法など、仕事をする上で役に立つようなヒントが隠れていることが多いということも実感しました。

今後も身につけた読書習慣を活かして読書を継続し、仕事に役立てることはもちろん、周囲へプラスの影響も与えられればと思います。


CSVからモデルオブジェクトを生成する際に気をつけたこと

$
0
0

こんにちは、クックパッド編集室の加々美です。

現在、食や暮らしのトレンドを発信するメディアであるクックパッドニュースの開発に携わっています。

クックパッドニュースは、1週間に100本以上の記事を配信しています。
このように比較的多くの記事コンテンツを作成する際、記事の基本的なパラメータ(例えば配信時間や記事の執筆者)をWebアプリケーション上で一つ一つ設定して記事を作成するのは時に煩雑な作業になりがちで、特に編集スタッフにとっては、スプレッドシート上で記事のパラメータを設定できた方が分かりやすく、作業がより確実になる場合があるかと思います。
(また、スプレッドシートであれば楽に複数人で編集できるというメリットもあります)

今回は、スプレッドシートからエクスポートしたcsvを用いて、モデルオブジェクトを生成する際に気をつけたことを紹介します。
※ 本稿ではGoogle DriveのスプレッドシートからエクスポートしたCSVを用いることを前提としています。

例として、タイトル(string型)、執筆者(integer型)、公開日時(datetime型)といった属性を持つArticleオブジェクトを生成することを考えてみます。

以下のようなスプレッドシートから、適宜バリデーションなどを行いつつArticle(記事)モデルを生成することを目指します。 執筆者に関して、ArticleモデルはEditor(執筆者)モデルとの関連を持っているものとします。 f:id:fkagami:20160309202935p:plain

最低限のバリデーションは、スプレッドシート側で行う

例えば日時など、一定の書式で縛りたい値がある場合、Googleスプレッドシートはエクセル同様、入力された値の書式の検証をすることができます。 メニューから「データ → 検証」を選択し、検証したい書式を設定すれば、特定の行列で異常な値を入力できないようにしたり、警告を表示させることができます。 f:id:fkagami:20160309203002p:plain

これによって「2016//01/01」、「10;00」等といった、パースエラーを引き起こしうる不適切な値を入力することを事前に防ぐことができます。

ActiveModelをincludeして、Railに従う

データベースと紐付かないモデルであっても、モデル内でActiveModelをincludeすることで、通常のモデルと同様に、バリデーション等の機能を使うことが出来ます。

以下のCsvArticleモデルは、インポートしたCSVの1行1行が、CsvArticleモデルオブジェクトに対応するように実装しています。 このように実装することで、CsvArticleも通常のモデルと同じようにバリデーションを行いモデルにエラーを追加することで、View上で特定のCsvArticleに発生したエラーの確認をすることができるようになります。

※ 分かりやすくするために実装をある程度簡略化しています

csv_article.rb

require"csv"classCsvArticleincludeActiveModel::Modelattr_accessor:title, :editor_name, :published_at# 通常のモデルと同じようにバリデーションを設定できる
  validates :title, presence: true
  validates :editor_name, presence: true
  validates :published_at, presence: true# editorsテーブルに、入力された執筆者に該当するものがあることをバリデーションする
  validate :validate_editor_exsistenseclass<< self# CSVを元に生成された、CsvArticleの配列を作成defcreate_list_from_csv(file)
      csv_articles = []
      CSV.foreach(file.path, encoding: "UTF-8", headers: true, converters: :integer) do |row|
        values = row.to_h
        published_at = parse_date_time(values["公開日"], values["公開時間"])
        csv_articles << CsvArticle.new(
          title: values["タイトル"],
          editor_name: values["執筆者"],
          published_at: published_at,
        )
      end
      csv_articles
    endprivatedefparse_date_time(date, time)
      Time.parse("#{date}#{time}")
    endenddefconvert_articleArticle.create!(
      title: title,
      editor: editor,
      published_at: published_at,
    )
  endprivatedefeditorreturnnilif editor_name.nil? || @editor_not_found@editor ||= begin# Editorモデルのname attributeが、editor_nameに合致するレコードを取得する
      editor = Editor.find_by(name: editor_name)
      @editor_not_found = trueunless editor
      editor
    endenddefvalidate_editor_exsistense
    errors.add(:editor_name, "#{editor_name}」が存在しません") if editor_name && editor.nil?
  endend

csv_articles_controller.rb

classCsvArticlesController< ApplicationControllerdefshowenddefcreate@csv_articles = CsvArticle.create_list_from_csv(params[:file])

    if@csv_articles.all?(&:valid?)
      @csv_articles.each do |csv_article|
        csv_article.convert_article
      end
      flash[:ok] = "#{@csv_articles.size}件、作成しました"
      redirect_to articles_path
    else
      render :showendend

show.html.haml

...
    - if@csv_articles&& @csv_articles.any?(&:invalid?)
      -# 2 is csv's row start num
      - @csv_articles.each.with_index(2) do |csv_article, row_num|
        - if csv_article.invalid?
          - csv_article.errors.full_messages.each do |message|
            -# エラーの発生した行と、エラーメッセージの内容を表示
            .flash_message.error_message
              #{row_num}行目: #{message}

      %table
        %thead
          %tr
            %th タイトル
            %th 執筆者
            %th 公開日
        %tbody
          - @csv_articles.each do |csv_article|
            %tr
              -# view上でも以下のようにしてエラーのあったカラムを特定できる
              %td{ class: ("error"if csv_article.errors.key?(:title)) }
                = csv_article.title
              %td{ class: ("error"if csv_article.errors.key?(:editor_name)) }
                = csv_article.editor_name
              %td{ class: ("error"if csv_article.errors.key?(:published_at)) }
                = csv_article.published_at
...

このように、ActiveModelを活用すると、データベースと紐付かないオブジェクトも楽に扱うことができます。
以上、サンプルコードと共に、CSVからモデルオブジェクトを生成する例を紹介しました。

クックパッドxMESH「未来のクッキングを描くアイデアソン」を開催します!

$
0
0

こんにちは。クックパッドCTO室の住 朋享(すみ ともみち)です。

皆様にあまり馴染みのない部署名かと思いますが、IoTなどを中心に未来の技術を考えながらクックパッドの未来を思い描く・・・といったお仕事をしております。

今回、クックパッドとIoTでどのような未来が創造できるかトライアルのひとつとして、ソニー Mesh Project Teamと共同で、3月21日(月曜/祝日)にアイデアソンを開催することになりました!

イベントについて

https://connpass-tokyo.s3.amazonaws.com/thumbs/69/5b/695bec04390baeb42ad4bf412034af46.png

日本最大のレシピサービス「クックパッド」と、“あったらいいな”を簡単に実現するソニーのDIYツールキット「MESH」(メッシュ)で、未来のクッキングスタイルや調理器具、キッチン空間などを思い描き、プロトタイプ(試作物)を一緒に創りましょう!

優れたアイデアを出していただいたチームには、素敵な賞品をプレゼントさせていただきます!

イベントの流れ1:料理を体験しながら課題抽出、アイデア出し

http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20150721/20150721172636.jpg

当日簡単ではありますがクックパッドのオフィスキッチンで一品程度の料理体験をしながら、料理に関する課題抽出をしつつIoTを絡めながら「みんなを笑顔にできるような未来のアイデア」を考えるアイデアソンです。

料理の得意不得意は問いません。「なぜ不得意だと思うのか」も大事な課題となると思いますので不得意な方も歓迎いたします。

イベントの流れ2:MESHを使ってモックアップ&プレゼン製作

https://first-flight.sony.com/images/pj/mesh/productDetail/usecase_1.png

MESHとはソニーさまが開発しているIoTの工作キットで、カラフルでかわいい電子ブロックそれぞれに動き、人感、温度などのセンサーが入っています。

MESHのタブレットアプリでそれらのセンサーが反応したときにそれがどのように動くのか”誰でも簡単に”設定できるのが最大の特徴で、「IoTを触ってみよう!」に持ってこいの製品です。

今回のアイデアソンでは、こちらのMESHを使いながら考えたアイデアを形にしつつアイデアソンを進めていきます。

最後にプレゼンテーション及び審査をしまして、ステキなアイデアを出していただいたチームにはステキな賞品を進呈させていただきます。

MESHについて詳しく知りたい!という方は以下のリンクをどうぞ meshprj.com

イベントにご興味のある方は!

開催日時

 2016年3月21日(月・祝)10:00-18:00

会場

 クックパッド本社(恵比寿ガーデンプレイス12F)
 東京都渋谷区恵比寿4-20-3

参加申込方法

イベント参加については以下のページにて受け付けております。

応募締め切りは3/14 23:30ですのでお早めにお願いします!

応募ページ:  http://connpass.com/event/27920/

みなさまの参加お待ちしております!

「IoTに触ってみたい!」「様々な参加者のアイデアに触れてみたい」「MESH面白そう」「新しいアイデア考えたい!」「クックパッドのキッチンで料理してみたい」などなど、みなさまの色々な想いとともに新しい価値を創る活動を広げていきたいと思っております!

今後も様々なイベントを開催できればと思いますので、引き続きよろしくお願い申し上げるとともに、みなさまのご参加おまちしております!

デザイン品質を高めるための仕組みづくり

$
0
0

ユーザーファースト推進室 デザイナーの橋本(@hashcc)です。

クックパッドでは、安定した品質のモバイルアプリケーションをユーザーさんに届けるために、デザインリリースマネージャという試みを2015年秋頃から始めました。

今回はこの試みについて発端や成果などをお話しします。

「あれ、なんでこんなデザインになってるの・・?」

クックパッドには日々多くのコード変更が加わっています。そうした中でも品質を安定させる(クラッシュや機能破壊を起こさない)ために、テストエンジニアなどが取り組んでいます。

デザイナーも「デザイン変更が伴う修正は必ずデザイナーがチェックする」というルールを作り、デザイン品質の安定化に努めていました。

f:id:vistawalk:20160311125144p:plain:right:w80

にも関わらず、リリース直前/直後になって「あれ、なんでこんなデザインになってるの・・?」と、想定外の変更・不具合に気づくということがありました。

振り返りをしてみると以下のような原因がありました。

  • チェック方法に漏れがあり、特定環境で意図せぬ表示になっていた
  • デザイン変更のない修正が実は不具合を起こしていて、表示が崩れてしまっていた
  • 変更によっては一部のデザイナーだけが把握していた(他の人は把握できていなかった)

また、漏れ/不具合の見過ごしがないようデザイナーが頑張ってみるにも、クックパッド内での開発量が多いため、レビュー対象を大きく広げるのは難しい状況でした *1

そのバージョンのデザイン変更に責任をもつ人を決める

チェック漏れや意図せぬ不具合は、事前に皆でさわってみればわかりますし、共有不足やお見合い事故は全体をきちんと俯瞰して見る人がいれば解決できそうです。

エンジニアは「リリースマネージャ」という形で、そのバージョンの担当者として全変更の進捗確認などを主に進める人を立てていて *2 、デザイナーもこれに倣って、そのバージョンのデザイン変更に責任をもつ人=デザインリリースマネージャを立てて進めてみることにしました

デザインマネージャの役割

Github Issueの巡回(デザインレビューの必要性や進捗の確認)リリース前のデザイナー内デザイン確認会の主催リリース後の影響の確認(主にレビューやユーザーさんからの意見を把握

※ Github Issueとは加える変更についてデザインなどを議論する掲示板のことです

実際に、これでデザイン上の問題を抱えたままリリースしてしまうことはある程度防ぐことが出来ました(と同時に後述しますがデザイン標準化という課題も見つけました)。

影響が大きい不具合を見つけた場合は、リリースするために修正が必要な不具合として、修正を依頼して進めていきました。

導入後の成果

この仕組みを試してみて、いくつか良いことがありました。

リリースサイクルへの理解が深まった

f:id:vistawalk:20160311125045p:plain

クックパッドのリリースサイクルについて

以前はアプリのリリースサイクルがあまり理解できておらず、開発期間が終了後に駆け込みで必要な実装をお願いするなど、迷惑をかけてしまうことがありました・・。

デザインリリースマネージャは、リリースサイクルにある新規Issue締切日・新規プルリクエスト締切日などを意識してIssue巡回や進捗確認をしていきますが、このお陰でリリースサイクルが行動レベルで理解できました。

現状と未来を振り返りやすくなる

f:id:vistawalk:20160311125115p:plain

各デザイナーは個々得意なエリア・サービスで仕事をしています。 もちろんそうしたエリア以外でも日々ドッグフーディングによりサービスに触れてみて改善していくことは重要です。ただ、サービスも増えてきていて、隅々まで触ることもなかなか難しくなってきました。

デザイナーが集まって最新バージョンをさわるデザイン確認会を定期的にやることで、「そもそもここってなぜこうなっているんだろう」「こうあるべきじゃないの・・?」という現状への理解と未来像を気軽に共有できるようになりました。

デザイン標準化の必要性に気づけた

デザインリリースマネージャが様々なデザイン関連Issueや画面デザインに目を通す中で、デザインルールが適用しきれていないところがあることがわかってきました。

そこで、ユーザーさんがより自然にコンテンツを見ていけるように、デザイン標準化Issueを立てて順次統一を進めています。

デザイン品質基準の共通理解が必要だと気づけた

f:id:vistawalk:20160311125048p:plain

前述のように、デザイン確認会で影響の大きい不具合が見つかった場合は、修正されるまでリリースしないようにしています。

何がそうした不具合にあたるかは現在はデザイナーが判断していますが、エンジニアが自律的に判断し動けるようなフローの方が望ましいでしょう。そのためにはデザイン品質の共通理解が必要で、それにむけて不具合の事例を積み上げたり、大事にしたい体験を明確にするといった取り組みを進めています。

この頃同時に行われた取り組み

デザインマネージャの取り組み以外にも、同時にデザインレビューフローを改善する取り組みもいくつか進んでいました。

デザインレビュー対象のIssueに気付きやすくする

f:id:vistawalk:20160311125051p:plain:w550

一部のエンジニアがデザイン変更を含むIssueに review_designというタグをつけてデザインレビュー対象であることをわかりやすくしていました。

実はあるIssueにデザイン変更があるかはパッと見てわからなかったりして見過ごしもあったのですが、そうしたことが減りました。

ただ、だんだん対象が増えて見過ごすことも増えてきたました。そこで、このタグが付けられた際に全デザイナーに通知がされるような仕組みへ改善することで、見過ごしづらくすることが出来ました。

f:id:vistawalk:20160311125111p:plain

個別機能のデザインレビューを行いやすくする

新機能のデザインレビューをする際、以前はエンジニアからスクリーンショットをもらうか、試せるように専用のビルドを作ってもらうよう依頼していました。

ただ、スクリーンショットですと一連の利用の流れで見ていくと不自然だったり、映っていない部分で問題が起きていることに気づけないなどの問題があります。また、専用ビルドを作ってもらうようお願いするのも、チェック回数が増えてくるとお互いに大変です。

f:id:vistawalk:20160311125108p:plain

そこで、新機能の実装毎=プルリクエスト毎にビルドを構築し配信できるようにして、普通にアプリを使っているようにレビューできるようにしました。また、プルリクエストのコメントに配信先URLが書き込まれるようになり、依頼しなくても気軽にレビューできるようになりました。

おわりに

以上、デザインリリースマネージャ導入の背景、施策、成果についてご紹介しました。

試して半年でまだまだ改善する点は多いのですが、以前に比べデザインチェック漏れなどによりデザイン品質を低下させてしまう事態は減らすことができました。

クックパッドでは、今はない価値をユーザーさんに届けるために日々試行錯誤を繰り返しています。 ただ、そのためには土台として、安定した品質で継続的に価値を届けられる体制と、的確な現状認識・未来像を持つことが必要になります。その土台を作るためにデザインリリースマネージャは有用だと考えていますが、この他にもできることがあるのか考え続けていきたいです。

*1:細かな変更も考慮しますと、デザイン変更はiOS&Androidで 20-30前後/1リリースのデザイン変更があります。実際はレビューと修正のイテレーションになるので、この2,3倍くらいの回数を確認していきます

クックパッドにおける最近のMicroservices事例

$
0
0

こんにちは。技術部の吉川です。

最近ではMicroservicesという言葉もかなり浸透し、そのテクニックも体系化されつつあります。 一方でMicroservicesについての話は概論や抽象的な話が多く、具体像が見えないという方もいらっしゃるのではないでしょうか。

当ブログでは1年半ほど前にMicroservicesへのとりくみについてご紹介しました。 当時社内ライブラリだったGarageはその後オープンソースとして公開され、また社内のシステムも当時と比べ飛躍的な進化を遂げています。

そういったクックパッドにおける最近のMicroservices事例を先日Microservices Casual Talksで紹介しました。

Microservicesの抽象的な話は一切割愛し、具体的な事例に終始した内容となっています。 Microservicesの基本となる考え方はわかったものの、実践方法で悩んでいる方へ少しでも助けになればということで当ブログでもあらためてご紹介します。

サービスの粒度

単一の責務をもった小さなサービス、などとよく言われますが、単一の責務とはどこまでを指すのでしょうか? クックパッドでのサービスを分類すると主に3つのタイプに分類できます。

ユーザーサービス

プロダクトにフォーカスしたタイプのものです。Microservicesは事業ドメインで分割することが大原則なので、これが基本形となります。 おいしい健康料理教室料理動画クックパッドブログみんなのカフェなどなど、多くのサービスを提供しています。

f:id:adorechic:20160315190103p:plain

個々のサービスは普通のRailsアプリケーションが多い(モバイルアプリのみというケースもありますが)ものの、チームによって個性が出ています。 例えばES6でReact.jsなものもあればCoffeeScriptのものもありますし、Rubocopを入れているものとそうでないものがあります。

プロダクトが異なれば対象ユーザーも違うため、柔軟な技術選定ができるべきです。一方で必要に応じて他ドメインのモデルを簡単に利用できれば相乗効果が期待できます。 言い換えると、ユーザーに対してはサービスごとに千差万別の対応ができつつも、社内の他サービスに対しては1つのモデルとして同じように振る舞うことが必要です。

ビューサービス

ドメインモデルは同じで別のビューとして振る舞うタイプです。

f:id:adorechic:20160315190101p:plain

形としてはいわゆるBFF(Backend for frontends)パターンに近いのですが、 クックパッドでは様々なデバイスに対応するためというよりは、同じドメインモデルを別バージョンで提供するようなケースで使われています。 OEM版がその例で、根本的には同じものを提供するが、UIが別バージョンだったりアカウント体系が異なるものを提供します。

共通基盤サービス

様々なサービスから共通して利用される基盤機能群です。例えば以下のようなものがあります。

  • OAuthプロバイダ
  • 決済
  • Push通知/メール配信
  • アクティビティ
  • 各種ログ
  • セキュアデータ(個人情報)ストレージ
  • 動画

サービス間の連携

RESTful Hypermedia API

Microservicesにおいてサービス間の通信をどのように実現するかはよく議論されるテーマです。 HTTPでREST APIを利用するのが一般的だと思いますが、Protocol BuffersやThriftなどを利用したRPCを選択する場合もあるかと思います。

クックパッドではGarageを使ったRESTful Hypermedia APIによるサービス間通信を行っています。 Garageについては以前にも紹介しているため本記事では割愛します。 本記事では、Garageだけではカバーできないポイントについて説明します。

並行処理と耐障害性

サービス連携が増えると問題になるものの一つがオーバーヘッドです。モノリシックなアプリケーションだとDBに接続してとってくるだけですが、それを単純にREST APIに置き換えるとオーバーヘッドは大きくなります。 スループットを上げるための工夫が必要となるわけですが、Microservicesアーキテクチャの場合他サービスへのリクエストは呼び出し元からするとただのIO待ちと見なせるため、並行処理と相性が良いという特徴があります。 適切な粒度にサービスが分割されていれば、各サービスの責務は独立性が高くなるため、より並行処理しやすい形になっていきます。

二つ目の問題は耐障害性です。どこかのサービスが詰まるとそのタイムアウト待ちリクエストが滞留してクライアント側も詰まってしまうという障害の連鎖はよくあるパターンです。 せっかくサービスが分かれているのに、障害に引きずられたくはありません。

リクエストを並行処理したり、よしなにリトライしたり、あるサービスで一定以上のエラーが発生したら、一定期間そのサービスへのリクエストを停止する。 こういったことをやってくれるのがExpeditorです。これらの機能で察する方もいるかと思いますが、Netflix/HystrixのRuby版です。 現状はまだ本家ほどの機能はありませんが、依存関係のあるAsynchronous ExecutionやFallback, Retry, Circuit Breakerといった基本的な機能は備えています。

サービス間のテスト

REST API連携で問題になるものの一つが、APIの互換性です。

通常CIで担保するテストのレベルでは、別サービスへのリクエストはスタブしているため、そのサービスのエンドポイントに互換性の無い変更が入っても気付けません。 Protocol BuffersでRPCをしている場合などは、protoファイルを共有していれば問題にならないかもしれません。 もしREST API連携で似たようなことをするなら、JSON Schemaを利用する手もあります。しかしもともとRuby + HTTPという文化であるため、型で管理する開発スタイルよりも、動くテストで管理するスタイルの方が開発者に馴染んでいました。 それがRack::VCRができた背景です。しかしサービスごとのCIで担保する方式だと、サービス間でビルド頻度に差がある場合、クックパッドのように高頻度でリリースを行う組織だと非互換を検知する前に反映されてしまうことがあります。 そこでConsumer-Driven Contract testingに舵を切り、現在ではPactに移行しました。

f:id:adorechic:20160315190102p:plain

これにより、クライアントが必要なAPIの互換性が壊れた場合、プロバイダ側のCIで検知できるようになり、非互換な変更がリリースされることを防ぐことができるようになっています。

サービス間のログを紐付ける

サービスがわかれていると難しくなることの一つがログの追跡です。特にエラーログは、あるサービスでエラーが発生した際に、その呼び出し元でのエラーを見たり逆にリクエスト先でどういうエラーが発生していたのかを追跡できないと、問題の特定が困難になります。 これはリクエストにIDを採番して、各サービスでどのリクエスト起因なのかを特定する手法が一般的です。特にRailsではデフォルトでX-Request-Idヘッダに対応しており、IDがなければ採番されるようになっています。 また互いにGarageClientを使って連携しているため、GarageClientがリクエスト時にX-Request-Idをセットすることでリクエスト先に伝播することができます。

社内ではエラーログの管理にSentryを使っており、このリクエストIDをタグとしてエラーにつけて管理するようにしています。

サービスの実行環境/構成管理

サービスの種類が増えてくると大変になるのはインフラです。クックパッドではもともとPuppetを用いて構成管理をしていましたが、構成するモジュールが増えて複雑になったり、 また同じアプリケーションであってもAPサーバー向けの構成と、バッチジョブワーカー用の構成を分けて管理したりと様々な面で複雑になっていました。

現在は、以前からある巨大なRailsアプリケーションなどといった一部を除き、ほとんどのアプリケーションがDockerで管理されています。もちろんテスト環境だけではなく本番環境もDockerです。

ビルドパイプライン

各サービスでの変更がマージされると、各サービスのCIが実行され、テストが通ればDockerイメージが作成されます。作成されたイメージはそのまま自動でstagingにデプロイされます。 もちろん本番も同じイメージが使われますし、またバッチジョブもサービス専用の実行環境を必要とせず、Dockerイメージをとってきて実行するだけです。 もちろんこのイメージは開発者の手元で動かすこともできます。スケールアウトする際や、ペネトレーションテストのために独立した環境を作りたい、といった場合にもすぐに対応できます。

デプロイ

DockerのバックエンドはECSを利用していますが、実際のデプロイにはELBとコンテナの紐付けを設定したり、環境変数の注入といった様々な作業が必要となります。 そういったデプロイに関連する様々なアクションをプラガブルに設定できるデプロイツールがHakoです。 Hakoのおかげで、初回デプロイ時にELBを作成したり、Route53を使って自動でドメインを設定したりといった、以前であればインフラエンジニアが個別に設定していたような作業まで自動化されています。 これを活用して、本番環境では1つのアプリケーションが1つのELBに紐づくが、テスト環境では1つのELBに複数のアプリケーションが紐づくといった柔軟な構成をとることができるようになっています。

設定管理

同じイメージといっても、DBの接続先情報や、APIのクレデンシャルなど、環境依存の部分もあります。 Hakoが環境変数を注入してくれるのである程度はそこで管理できるのですが、秘匿すべき値はetcenvetcvaultを使って管理しています。 バックエンドはetcdですが、もともとACLが無かったため暗号化して管理するようにしたものです。etcenvにはetcwebというUIツールがあり、それを使ってWebUI上で管理しています。

組織的なサポート

Microservicesアーキテクチャを採用しているということは、当然ながらチームもサービスに応じて分かれています。 そのためチーム間の連携方法や、全体の意思決定などの仕方も徐々に変化してきました。

技術領域課題共有会

各チームの独立性が高まると、互いにどういうことをやっているのか把握しづらくなってきます。その事業固有の悩みも増えていきます。 そうなると、実はみんな困っているのに自分たちのところだけだと思って問題提起されなかったり、隣のチームで解決済みなのを知らずにずっと困っているということにもなりかねません。 そこで各チームの技術リーダーと、基盤/インフラのエンジニアが月に一度集まって課題を話し合う場を設けています。

この会では各チームから今困っていることをあげてもらい、既に解決策がある場合はその共有を、無い場合は適切なチームが対応に動きます。 また全体最適ということで全体に影響のある仕様変更を考えているときに共有したり、それ以前にヒアリングするような場にもなっています。 これによって、問題と思ってなかった点に実はみんな困っていたとか、逆にみんな使っていると思って頑張って運用していたら別に誰も使っていなかったということも見つけ出せるようになってきました。

技術基盤担当

チームが多様になると、中には基盤改善が得意なメンバーがいるチームといないチームがあったり、新規開発が多いチームは自然と最新の社内基盤を知っていますが、 そうでないチームは便利な社内基盤があるのを知らずに自前で頑張ったりするケースもあります。あるいは基盤改善にまわす余力が無いチームもあったりします。

そこで開発基盤のメンバーが各サービスに担当者としてつくようにしました。 一人で複数のサービスについており、常駐するわけではないのでチーム外基盤担当とでも言えるでしょうか。 担当メンバーは各チームのチャットにはいるようにしているので、雑に相談したり、時にはガッツリはいって開発環境の整備をしたりします。

まとめ

クックパッドにおける最近のMicroservices事例についてご紹介しました。 Dockerがもたらしたポータビリティは社内の開発スタイルを大きく変え、Microservices化を加速させました。 それに呼応してPactのような仕組みや組織的なサポートなどが発展し、クックパッドの開発スタイルはこの1年でも飛躍的な進歩を遂げています。

Microservices化によって各チームがそれぞれのプロダクトにフォーカスすることができるようになります。 プロダクトにフォーカスするということはそのプロダクトのユーザーにフォーカスするということです。 プロダクトごとに最適な技術を選択したり、自分たちで自分たちのプロダクトに責任と権限を持って開発することができます。

今回は概要をご紹介しましたが、いつかHakoやPactといった個々の仕組みについてもご紹介できればと思っています。

Elasticsearch の Percolator を使った地理属性判別システムの構築

$
0
0

こんにちは、ホリデー株式会社の内藤です。Holiday ( https://haveagood.holiday/ ) というサービスの開発を行っています。

先日開催した Cookpad TechConf 2016では、『おでかけスポット検索のむずかしさ - Holiday を支える検索技術』という題で発表を行いました。

www.slideshare.net

この発表では、

  • おでかけスポットの検索では、全文検索だけでは満足のいく結果は得られない
  • 地理空間検索に拡張することでよりよい検索体験を作ることが可能
  • これを実現するための Elasticsearch の機能を紹介

というような内容を紹介しました。

例えば、我々が「中目黒」を思い浮かべた時にイメージするエリア内の住所には、「中目黒」という文字列が含まれていません。 また、「奥渋谷」のように住所としては特定できない「だいたいこの辺り」というようなエリアも存在します。 あるいは、「中目黒駅」と調べたユーザは、「駅からの距離を考慮した並び順で結果を見たい」と考えていそうだと想像できるものの、文字列からは距離を計測することはできません。

このように、検索クエリを文字列として扱うだけでは、検索クエリに込められたユーザの意図を汲みとった検索結果を返すことが難しくなります。

以前の記事でも紹介したように、Elasticsearch には位置情報を利用した検索機能が用意されています。 これらの機能を活用することで、全文検索と地理空間検索を組み合わせて、より満足度の高い検索体験を作ることができます。

ただ、これら地理空間検索機能は、公式ガイド 『The Definitive Guide』でも言及されているように、文字列マッチなどの検索条件に比べて計算コストの高い処理になります。

Geo-filters are expensive — they should be used on as few documents as possible. First remove as many documents as you can with cheaper filters, like term or range filters, and apply the geo-filters last.

このような高コストな処理を検索時に毎回行うのはあまり望ましくありません。 できるならば高負荷な処理は事前に済ませておき、検索時には低コストな処理のみを行いたいという思いがあります。

今回は、このニーズを満たすために、Elasticsearch に用意されている Percolator という機能を使ってみようと思います。

Percolator とは

通常の検索時には、あらかじめ登録したドキュメントに対して検索クエリを発行することで、目的の情報を探します。 一方で Percolator はこれとは全く逆で、あらかじめ登録しておいた検索クエリに対してドキュメントを当てることで、合致する検索条件がないかを判定します。

よくある使い方としては、例えばECサイトにおいて、ある商品に興味があるユーザに対して、その商品が入荷された時にお知らせを送るというようなケースがよく取り上げられます。

今回は上記のような使い方ではなく、あるスポットが「どういったエリアに属するのか」や「どの駅の周辺に位置しているのか」といった属性情報を、Percolator の仕組みを使って取得してみようと思います。

なお、実行環境は以下のとおりです。

  • Elasticsearch 2.1 *1
  • Ruby 2.2.4
  • Rails 4.2.6
    • クライアントには elasticsearch-rails gem を使用

本稿で使用するサンプルコードは、GitHub上で公開しています。

データを準備する

まずは必要なデータを準備します。

今回は、以下のようなスキーマを使います。

ActiveRecord::Schema.define(version: 20160314093426) do

  create_table "areas", force: :cascadedo |t|
    t.string   "name"
    t.text     "coordinates"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: falseend

  create_table "spots", force: :cascadedo |t|
    t.string   "name"
    t.string   "address"
    t.decimal  "lat",        precision: 9, scale: 6
    t.decimal  "lon",        precision: 9, scale: 6
    t.datetime "created_at",                         null: false
    t.datetime "updated_at",                         null: falseend

  create_table "stations", force: :cascadedo |t|
    t.string   "name"
    t.decimal  "lat",        precision: 9, scale: 6
    t.decimal  "lon",        precision: 9, scale: 6
    t.datetime "created_at",                         null: false
    t.datetime "updated_at",                         null: falseendend

サンプルデータは db/seeds.rb内に用意しているので、以下のコマンドを実行すれば必要なデータが作られるはずです。

$ bundle exec rake db:create
$ bundle exec rake db:migrate
$ bundle exec rake db:seed

おでかけスポットの情報は Elasticsearch 上では次のようなスキーマで格納することにします。

classSpot< ActiveRecord::BaseincludeElasticsearch::Model

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

  mapping do
    indexes :id, type: 'string', index: 'not_analyzed'
    indexes :name, type: 'string', analyzer: 'kuromoji'
    indexes :address, type: 'string', analyzer: 'kuromoji'
    indexes :location, type: 'geo_point'enddefas_indexed_json(options = {})
    { 'id'          => id,
      'name'        => name,
      'address'     => address,
      'location'    => "#{lat},#{lon}",
    }
  endend

Elasticsearch にドキュメントを登録するため、以下のタスクを実行します。

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

これで、事前準備は完了です。

あるスポットを含んでいるエリアを取得する

では、あるスポットを含んでいるエリアを取得してみます。

今回、「中目黒エリア」を以下の青枠で囲まれた範囲と設定しました。

f:id:qtoon:20160317081243p:plain:h300]

この多角形のエリアデータは以下のように表現されます。

areas = [
  {
    name: "中目黒エリア",
    coordinates: [
      [139.69300746917725, 35.64788092832728], # 最初と最後は同じ値を指定する
      [139.6939516067505, 35.644114489675275],
      [139.69064712524414, 35.64059201137997],
      [139.69892978668213, 35.63846449878154],
      [139.70317840576172, 35.643033349486984],
      [139.70073223114014, 35.64721832699104],
      [139.69300746917725, 35.64788092832728]  # 最初と最後は同じ値を指定する
    ]
  }
]

Area には名称 nameと多角形の頂点を表す座標データ coordinatesを持ちます。 この coordinates[経度, 緯度]からなる配列で、DBにはシリアライズされた状態で格納されます。

classArea< ActiveRecord::Base
  serialize :coordinatesend

検索クエリをインデックスする

ある座標が特定の多角形に含まれるかどうかを検索するには、Geo Polygon Queryを使います。

elasticsearch-rails gem を使った場合、Percolator の登録は次のように行います。

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

  # id: 検索クエリ毎にユニークなID# body: 検索条件defself.index_percolator(id, body)
    args = {
      index: self.__elasticsearch__.index_name,
      type: '.percolator',
      id: id,
      body: body,
    }
    self.__elasticsearch__.client.index(args)
  endend

このように、Percolator の登録処理は、通常のインデックス処理とほとんど変わらず、違いは .percolatorという特別な type 名を用いるということだけです。

呼び出し側の記述は以下のようになります。

classArea< ActiveRecord::Base
  ...

  defself.create_polygon_percolatorsArea.all.each do |area|
      id = "area-polygon-#{area.id}"# e.g. `area-polygon-1`
      body = {
        query: {
          filtered: {
            query: {
              match_all: {}
            },
            filter: {
              geo_polygon: {
                location: {
                  points: area.coordinates
                }
              }
            }
          }
        }
      }
      Spot.index_percolator(id, body)
    endendend

では、rails console を起動し、このメソッドを実行します。

$ rails console

Area.create_polygon_percolators

これで Elasticsearch 上の Spot インデックスに対して、Geo Polygon Queryの検索条件が登録されました。

実行する

では、「スターバックスコーヒー中目黒駅前店」というスポットが「中目黒エリア」に属しているかを実際に確かめてみます。 このスポットは中目黒ゲートタウンタワー内に位置するため、正しく動作していれば「含まれる」と判定されるはずです。

f:id:qtoon:20160317081313p:plain:h300

elasticsearch-rails gem を使った場合、Percolator クエリを実行する処理は以下のように記述します。

classSpot< ActiveRecord::Base
  ...

  defpercolateSpot.__elasticsearch__.client.percolate(
      index: Spot.__elasticsearch__.index_name,
      type: Spot.__elasticsearch__.document_type,
      body: {
        doc: {
          location: {
            lat: lat,
            lon: lon,
          }
        }
      }
    )
  end

では、rails console を起動し、以下の処理を実行してみます。

$ rails console

spot = Spot.find_by(name: 'スターバックスコーヒー中目黒駅前店')

spot.latlon
=> [35.643602, 139.699077]

spot.percolate
=> {
  "took" => 1,
  "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 },
  "total" => 1,
  "matches" => [
    { "_index" => "development-espercolatorsample::application-spot",
      "_id" => "area-polygon-1"
    }
  ]
}

このように、 レスポンス内の matchesフィールドに先ほど指定したIDが含まれていることがわかります。 matchesには、このドキュメントを解とする検索条件のIDが含まれます。

では、「恵比寿ガーデンプレイス」が「中目黒エリア」に含まれているかどうかを確かめてみましょう。

spot = Spot.find_by(name: '恵比寿ガーデンプレイス')

spot.latlon
=> [35.642186, 139.713309]

spot.percolate
=> {
  "took" => 1,
  "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 },
  "total" => 0,
  "matches" => []
}

当然これは「含まれていない」と判断されます。

以上のように、事前に登録しておいた Geo Polygon Queryに対してスポット情報(緯度経度)を投げることで、そのスポットを解とする検索条件を取得することができました。 あとは、検索条件のIDからエリアIDを抽出すれば、あるスポットがどのエリアに含まれるのかを知ることができます。

周辺の駅を特定する

次に、あるスポットがどの駅の周辺にあるのかを判別します。

検索クエリをインデックスする

ある中心点からの一定距離内に含まれる地点を検索するのには、 Geo Distance Queryを用います。

classStation< ActiveRecord::Basedefself.create_distance_percolators(radius: 1000)
    Station.all.each do |station|
      id = "station-distance-#{station.id}-#{radius}"# e.g. `station-distance-1-1000`
      body = {
        query: {
          filtered: {
            filter: {
              geo_distance: {
                location: {
                  lat: station.lat,
                  lon: station.lon,
                },
                distance: "#{radius}meters",
              }
            }
          }
        }
      }
      Spot.index_percolator(id, body)
    endendend

先ほどと同様に、rails console を起動し、このメソッドを実行します。

$ rails console

Area.create_polygon_percolators

これで Geo Distance Queryの検索条件も、Percolator として登録することができました。

実行する

では、「スターバックスコーヒー中目黒駅前店」というスポットが、どの駅の周辺(半径1000m以内)にあるのかを調べてみます。

spot = Spot.find_by(name: 'スターバックスコーヒー中目黒駅前店')

spot.latlon
=> [35.643602, 139.699077]

spot.percolate
=> {
  "took" => 1,
  "_shards" => { "total" => 5, "successful" => 5, "failed" => 0 },
  "total" => 2,
  "matches" =>[
    { "_index" => "development-espercolatorsample::application-spot",
      "_id" => "station-distance-1-1000"# station_id: 1 => 中目黒駅
    },
    { "_index" => "development-espercolatorsample::application-spot",
      "_id" => "station-distance-3-1000"# station_id: 3 => 代官山駅
    },
    { "_index" => "development-espercolatorsample::application-spot",
      "_id" => "area-polygon-1"
    }
  ]
}

このように、中目黒駅(ID: 1)と代官山駅(ID: 3)がレスポンスに含まれています。 これは、このスポットがこれらの駅から半径1000m以内の位置に存在していることを示しています。

また、実行結果をよく見てみると、先の段落で作成した Geo Polygon Queryについても、マッチした条件に含まれていることが分かります。

Percolator を使うと、あるドキュメントを一度投げるだけで、そのドキュメントを「検索条件に合致している」とみなすすべての検索クエリを一気に取得することができるのです。

まとめ

Percolator では、これまで検索時に使っていたクエリを .percolatorという type 名でインデックスするだけで使えるようになります。 後は、#percolateメソッドを呼び出すだけで、そのドキュメントの属性情報を取得することが可能です。

以上のように、Percolator を用いるとコンテンツの属性情報を得るコードを新規に書かず Elasticsearch 内に共通化できるので、シンプルな形で実現することができます。

今回紹介した使い方以外にも、不適切なワードを含むコンテンツの投稿を検知したり、投稿内容からキーワードを抽出して自動的にタギングするというようなことも可能です。

この記事が、Percolator を試してみようかなという方にとって少しでも参考になれば幸いです。

*1:執筆時点の最新版である2.2系ではバグがあって動きません。詳しくは issueをご覧ください。

ブラウザから使える O/R マッパ、 js-data を使ってみる

$
0
0

ヘルスケア事業部の濱田です。花粉がつらい時期ですが、みなさん楽しく開発してますか?

おいしい健康では、JavaScript(以下 JS)で非同期にサーバ側のリソース操作を行う際に、js-data というライブラリを使っています。Rails ユーザにとってはとっつきやすい便利なライブラリですが、日本語での情報がほとんど見当たらなかったため、簡単にご紹介したいと思います。

js-data とは

JS 製のデータ管理用ライブラリです。RESTful API などを通じて取得できるデータ(リソース)を抽象化して、CRUD 操作を統一したインターフェースで行えるようにしてくれます。 リソースごとにオブジェクト(モデル)を定義したり、findでデータを取ってくるなど、ActiveRecord などの O/R マッパのような使い勝手が特徴。設計等には Ember data の影響を受けています。 以下で、基本的な使い方を見てみましょう。

リソースとデータストアの定義

リソースとは、ActiveRecord で言えばひとつひとつのモデルにあたる JS オブジェクトで、以下のような機能を持っています。

  • リソース名、対応する API のエンドポイントの URL などのメタデータ管理
  • リソースに対する CRUD 用インターフェースの提供

API などを通じて読み込まれたデータは一旦データストアと呼ばれるオンメモリキャッシュに格納されます。 一度読み込んだデータはストア内から呼び出し、Update, Delete などの更新系操作のタイミングでサーバにデータを書き戻すというわけです。

例 - Book リソース

例として、一冊の本を表す Book リソースを定義してみましょう。

// データストアを作成var store = new JSData.DS();

// 元データの参照元として HTTP (Web API) を指定する
store.registerAdapter('http', new DSHttpAdapter(), {default: true});

// データストア上に Book リソースを定義var Book = store.defineResource({
  name: 'book',  // リソース名
  endpoint: 'books'// 対応する API のエンドポイント});

RESTful API の準備

上記のように定義したリソースオブジェクトを使うには、操作を実際に行うための Web API も用意してやる必要があります。リソース定義には endpointという API のエンドポイントの情報しか含まれていませんが、その URL に json オブジェクトを返却する RESTful な API を定義しておけば、js-data が自動的に叩いてくれるようになっています。

定義しておく操作

以下のような API が存在することが期待されています。必ずしも全部実装しておく必要はありません。使用したいものだけで OK です。

operation URL HTTP method Description
Create /<resource> POST 新しいアイテムの作成。プロパティ({ title: '眠る盃' })を受け取り、作成したアイテムのデータを返す({ id: 1, name: '眠る盃' })。
Read /<resource>/:id GET 既存のアイテムの取得。 { id: 1, title: "眠る盃" }
Update /<resource>/:id PUT 既存のアイテムの更新。プロパティ({ title: '父の詫び状' }など)を受け取り、更新したアイテムを返す({ id: 1, title: '父の詫び状' })。
Delete /<resource>/:id DELETE 既存のアイテムの削除。
Read All /<resource> GET 指定された条件でリソースを検索し、配列で返す([{ id: 1, title: '父の詫び状' }, { id: 1, title: "眠る盃" }])。検索条件は GET パラメータとして指定。
Update All /<resource> PUT 指定された条件で検索された複数のリソースを一括更新。更新されたリソースの配列を返す。
Delete All /<resource> DELETE 指定された条件で検索された複数のリソースを一括削除。

リソースの CRUD インターフェイス

データストア/リソースの定義、対応する API の実装を終えれば、実際にリソースの操作を行うことができるようになります。

// Readvar book = null;
Book.find(1).then(function (data) {
  book = data;
  book; // { id: 1, title: "眠る盃", publishedIn: "1979" }});

// Createvar book = null;
Book.create({
 title: "父の詫び状",
 publishedIn: "1988"}).then(function (data) {
  book = data;
  book; // { id: 2, name: "父の詫び状", publishedIn: "1988" }});

// Update
Book.update(2, {
  publishedIn: "1978"}).then(function (data) {
  data; // { id: 2, name: "父の詫び状", publishedIn: "1978" }});

// Read Allvar books = [];
Book.findAll({// 検索条件の指定。// get パラメータの "?where=..." に JSON 文字列として埋め込まれるので、// サーバではこれをパースして配列にした後、検索条件に使用する
  where: {
    publishedIn: "1979"}}).then(function (data) {
  books = data; // 配列が返る。[{ id: 1, title: "眠る盃", publishedIn: "1979" }]});

ポイント

非同期なメソッドは Promise を返す

API コールを伴う操作は非同期に行われ、Promise パターンを使ってメソッドチェインしながらデータを受け取ることになります。

データの指定をするシンタクスがある

findAlldestroyAllなど、複数のデータを操作するメソッドにはデータを指定する独自のクエリシンタクスが用意されています。whereorderByなど、SQL を使ったことがある人なら馴染みやすいものばかりでしょう。ただし、このクエリを受け付けるための実装をサーバに加える必要があるので、使わないという選択肢も可能です。 このシンタクスは、データストアの中にあるデータのみを対象に検索を行う Resource.filterメソッドなどでも共通で使えます。

2度目以降の取得は直接データストアから

一度読み込んだデータはデータストアにキャッシュされています。findを複数回行っても、API コールが発生するのは最初の一度で済むので効率的ですね。何らかの理由で強制的にデータを読み込み直したいときは、以下のようにオプションを渡せば OK です。

book; // { id: 1, title: "眠る盃" }
Book.find(1, {
  bypassCache: true// このオプション指定でキャッシュを使わずリソースを読み込み直す}).then(function (data) {
  data; // { id: 1, title: "眠る盃" }})

リレーションの定義

リソース同士の間に関連を定義して、データストア内でひも付けておくことができます。

例 - Author リソース

例として先ほどの Book に対する Author リソースを定義し、関連を定義してみましょう。

// データストア上に Author リソースを定義
Author = store.defineResource({
  name: 'author',
  endpoint: 'authors',
  relations: {
    hasMany: {
      book: {// author.books でアクセス
        localField: 'books',
        // book リソースにおける、author を参照するための外部キー相当のプロパティを指定
        foreignKey: 'authorId'}}}});

// Books リソースの定義を変更
Book = store.defineResource({
  name: 'book',
  endpoint: 'books',
  relations: {
    belongsTo: {
      author: {// book.author でアクセス
        localField: 'author',
        // book リソースにおける、author を参照するための外部キー相当のプロパティを指定
        localKey: 'authorId'}}}});

defineResourceを呼ぶときに、リレーション用のオプションを渡すだけです。データストアに入ったオブジェクトは、localKey/foreignKeyをもとに関連付けられ、localFieldに設定した名前でアクセスできるようになります。

関連リソースの操作

関連を定義したリソースを実際に操作してみましょう。

authorResponse = {
  name: "向田邦子",
  birthDate: "1929-11-28",

  books: [{ id: 1, authorId: 1, title: "眠る盃", publishedIn: "1979"},
    { id: 2, authorId: 1, title: "父の詫び状", publishedIn: "1978"}]}// inject はデータストアへ直接リソースを挿入するメソッド// (API 経由の create は行われない)
author = Author.inject(authorResponse); // { name: "向田邦子", birthDate: "1929-11-28" }
author.books; // [{ id: 1, authorId: 1, name: "眠る盃", publishedIn: "1979" }, { id: 2, ... }]

book = null;
// すでにデータストアに挿入されているので、API コールは起こらない。 { id: 1, authorId: 1, name: "眠る盃", publishedIn: "1979" }
Book.find(2).then(function (data) { book = data });

Book.create({
  authorId: author.id,
  title: "阿修羅のごとく",
  publishedIn: "1979"}).then(function(data){// 関連リソースを呼び出すと、新しいインスタンスも含まれている
  author.books; // [{ id: 1, ...}, { id: 2, ... }, { id: 3, name: "阿修羅のごとく", ...}]});

ポイント

API は関連データを一気に返却できる

上記の例でお気づきだと思いますが、レスポンスに関連モデルのデータを含めておけば、自動的にデータストアへの登録も行われます。この場合、author.booksのアクセスをした際にも API コールは行われず、データストア内のデータが返ってきます。関連データをネストして返しても js-data のリソースとして扱われるので、おしゃべりな APIになるのを防ぎつつ、js-data インタフェイスの恩恵をうけることができます。

関連リソースの変更は自動で反映される

新しい子データの作成/削除などでデータストア内の関連リソースの値が変更されると、すぐに反映されます。上記の例では、Book リソースが作成されたあと Author リソースからは単に .booksとアクセスするだけで新しい Book リソースが見えるようになります。

イベント

データの更新を検知するためのイベント通知の仕組みが用意されています。リソースのライフサイクルそれぞれのタイミングで違ったイベントが通知されます。

リスナの登録/解除などのインターフェイスは EventEmitterと同一で、onoffemitが用意されています。

新しいデータがデータストアに挿入されたときに通知を受けて何か処理を実行する例を書いてみましょう。

notifyNewBookTitle = function (resource, instance){
  console.log("新しい本が追加されました:" + instance.title)
}// DS.afterInject はデータストアに新しいインスタンスが入った時に呼ばれる
Book.on("DS.afterInject", notifyNewBookTitle);

Book.create({
  authorId: 1,
  title: "思い出トランプ",
  publishedIn: "1980"});

// => '新しい本が追加されました: "思い出トランプ"'

Book.off("DS.afterInject", notifyNewBookTitle);

Book.create({
  authorId: 1,
  title: "あ・うん",
  publishedIn: "1981"});

// => ''

その他便利な機能

ここでは紹介しきれませんでしたが、以下のような機能も実現されています。

  • リソースのバリデーション
  • Computed property
    • データ更新後一度だけ値を計算して保持しておく機能
  • localStorge 上にあるデータなど、HTTP ベースでないリソースへのアクセス

注意 - 検索性の悪さ

js-data は素晴らしいライブラリですが、少々一般的すぎる名前のせいか、検索エンジンで情報を探すのに苦労します。 ライブラリ名で検索しても、公式ページの Google での検索順位は 4 位。その他にひっかかってくるページもJavaScript で HTML5 の data 属性を操作する方法のページが多かったりと、なかなか思うように情報を集められません。

問題の解決策や情報がほしい時は、公式ドキュメント(とてもよくまとまっています)や Github issuesStack Overflowなどを直接検索すると良いと思います。公式の Slack チャンネルもありますので、ここで開発者に直接尋ねてみるのも手です。

おわりに

Redux などの Flux フレームワークを導入するまでもないんだけど、React.js と組み合わせてデータのアクセスの部分だけ抽象化してくれるライブラリはないかな……。ということで調査したのが、js-data を見つけるきっかけでした。 自分以外の特定のフレームワークに依存していないので、React.js で管理されたコンポーネントとそれ以外のコンポーネント、両方でこのライブラリをモデル層として使用することもできます。

名前で損をしている感じがあるのですが、なかなか使いやすいやつです。この機会にぜひ触ってみてください。

社内共用カメラのすゝめ

$
0
0

舘野 (id:secondlife / @hotchpotch) です。

クックパッドでは会社の中心にキッチンがあり、社員同士でランチやお菓子を作ったり、イベントを開いたりと社内のコミュニケーション用途で広く使われています。そんなキッチンで作られている様々な料理や、楽しそうなコミニュケーションをその場に居ない人にも伝えたいなー、どうにか伝える方法は無いのかな〜と思っていました。

そんな中、より良い組織を作るためにの中でも触れられているコミニュケーション改善の話をしている最中、社内に共用のカメラが置いてあって、撮った写真が何もせずとも自動で社員が見れる場にアップロードするだけの仕組みを提供するだけでうまく行くかも、と思ったので2014年末に作ってみました。

f:id:secondlife:20160323132837j:plain

サービスのコンセプト

作るときに盛り込んだコンセプトは以下の二点です。

  • 運用コストがゼロ
  • アップロードコストがゼロ

運用コストがゼロ

ここで言う運用コストは、人的な運用コストです。社内の誰かがそれなりに面倒を見て運用することになると「また余計な物作りやがって…*1」ということになりかねないですね。

そのため作った後、人の運用のコストが無いサービスにしようと思いました。

アップロードコストがゼロ

アップロードコストがゼロ、というのは人が意識的にアップロードする行為を入れない、と言うことです。UI で 1 を 0 にすると大きな変化が起こる、と誰かが言ってた*2のですが、「何か行為が必要」な事と「何もしない」ことは大きな隔たりがあります。

能動的なアップロードが必要だと、アップロードすること自体のコストと、どの写真をアップしようかという写真の取捨選択コストがかかってしまいます。

もっと気軽にアップロードされて欲しい、という思いから、意識的にアップロードしなくても良い、アップロードコストがゼロのサービスにしようと思いました。

このコンセプト二つは、人のコストを無くすという点では共通してますね。

プロトタイピング

まずはプロトタイピングを、ということでこの二つのコンセプトを満たしたサービスを作ってみました*3。アップロードコストがゼロという視点では EyeFi (旧バージョンの方で、現行バージョンは異なる仕様です)を使いました。 EyeFi(旧) は写真を撮るとすぐに EyeFi サーバにアップロードされ、そこ経由で様々なフォトストレージにアップロードすることが可能です。

運用コストゼロ、という視点からはアップロード先に Google Photos を選びました。Google Apps を会社で利用している場合、社員のみ閲覧可能にすぐ変更可能なことも便利ですね。

これで問題なければ…と思ったのですが、アルバム機能が新しい日付順でのソートの Permalink が保持できなかったり…等、今回の用途にそぐわなかったため、Google Photos はやめました。

※ありものを使い、最小コストでのプロトタイピング

f:id:secondlife:20160325133616p:plain

プロトタイピング その2 ~ 完成まで

続いては、EyeFi(旧) は自分のサーバのAPIを叩くこともできるので

  • Heroku の API サーバへ Post
  • Heroku 側で ImageMagick 経由でいくつかのサイズへリサイズ
  • ストレージへアップロード

という仕組みでストレージへファイル名を 2016/02/14/142010_{hash}.jpg のようなファイル名でアップロードします。なお裏側のストレージは、 http(s) の permalink にアップロードするだけでアクセスできる*4機能を持ち、またファイルリストがAPIで取れるサービスであればどれでも良かったので AWS S3 を選びました*5

閲覧側は API から画像一覧を取得し、それを元に permalink を組み立てれば完了です。社内に dokkuが立っていたので、画像一覧を返す JSON API をたて、html + JavaScript で UI を実装しとりあえずプロトタイピング完了です。

画像 URL が月日ごとの URL となってるので、各種ストレージのファイルリストを取得するオプションに prefix を指定(2016年2月の写真なら 2016/02/ など)することで、特定月や日の写真一覧の取得なども簡単にできますし、日時が permalink に入ってるので画像のメタ情報無く、permalink をソートするだけで写真の日時順に並べ替えが行えます。

と言うのも、何らかしらの情報をRDBやNoSQL等へ記録してしまうと、それを維持する運用コストが発生してしまうため、使わなくても問題ないような仕様にしました。

※軽く実装

f:id:secondlife:20160325133621p:plain

と言うわけで軽く実装してみて、要求は満たせそうなので JS + CSS で体裁を整え完成です。

f:id:secondlife:20160325133625p:plain

なお削除は、閲覧側の UI に削除ボタンを付け、アップロードされたけど消したい場合に誰でも気軽に消せるようになっています。

※気軽に削除できるUI f:id:secondlife:20160325133630p:plain

社内リリースまで

続いて、社内リリースのための周知です。作ってもそもそも認知されない物になってしまうと悲しいです。そのため、社内ポータルのトップ画面*6から XHR の CORSで 閲覧側の JSON API を叩き、ポータルのトップに最新の写真が出るようにしたり、この共用カメラはなんぞや?という解説ページを作りリンクを張るなど、興味を持ってもらうような導線を入れました。

f:id:secondlife:20160325133632p:plain

結果、当初のコンセプト通り、アップロードコストがゼロのため気軽撮られた写真が集まるようになり、一年間で5,000枚以上の写真がアップロードされ、いろいろ料理の写真や楽しそうな写真、イベントの写真等々が会社全体に伝わるようになりました。

最初の頃はシャッターを押したらどの写真もアップロードされてしまうことに慣れない人も居ましたが、ほとんどの写真はとりわけ削除が必要になるような写真では無いため、今では気軽に撮って、もしどうしても削除したい場合でも簡単に消せることから、慣れていったようです。

またもう一つのコンセプトである、人による運用コストゼロも、S3等のオブジェクトストレージしか使ってないため、クラウドサービス障害時のストレージ障害確認の必要性も無く、手間無く運用できているといえると思ってます。

使われなかった機能

社内向けツールのため、機能を自由に実装できるため、いろいろな機能を追加したくなる欲求に駆られます。そして実装してしまいました。説明した機能以外に

  • 撮った写真を社内チャットに流す
  • 写真を特定メールアドレスに送信するとアップロード
  • 写真をブラウザ上にドラッグするとアップロード

等々の機能を実装したのですが、一つ目は撮られた写真をリアルタイムのフローで見たいという欲求はあまりなく、リリースした当時はそれなりの人が居たのですが、今はそのチャットルームは寂れており、需要が無いことが解りました。Instagram 等の写真もリアルタイムで見たいというよりは、空いてる時間に見たい欲求の方が強いですし、仕事上 PC をいつも開いてる状態では、ブラウザ上の UI で俯瞰して見れた方が便利ですね。

また後者二つは、別のカメラやスマートフォンで撮ったファイルのアップロードにも使えるよね、ということで実装したのですが、アップロードコストがゼロのコンセプトに反していて、能動的なアップロードが必要になってしまい、やはりほぼ使われない結果に終わりました。

Tips・予備バッテリーを用意する

共用で誰でも使えるカメラのため、うっかり充電し忘れてカメラのバッテリーを使い切ってしまった状態になることがあります。カメラを使おうと思ったのに「撮影したいのに充電されてない…」という状況が発生してしまうと悲しいですよね。

共用カメラの置き場所のそばに外部充電器 + 予備バッテリーをおいておくことで、カメラ本体のバッテリーが切れていても即座に交換でき、また交換時についでに充電するので電池が切れるという事は基本ありません。撮りたいときに撮れないガッカリを減らすためにも、充電器 + 予備バッテリーがあると便利です。

まとめ

クックパッドでは共用カメラを用い、撮影後自動でアップロードして社内ポータルから閲覧できる仕組みを運用してます。このカメラがあることで、キッチンの楽しい、美味しい写真が社内で共有され、コミニュケーションの促進にも役立ってると思っています。

社内にこのようなカメラが一つあると何かと便利なので、皆さんの会社も気軽に撮影できてアップロードされる共用カメラを検討してみてはどうでしょうか。本エントリーでも紹介している旧版の Eye-Fi を使うことで、提携しているクラウドサービス等々にも撮影した写真をアップロードでき、便利です。

古いサービスは終了になりがちですが、 Eye-Fi はサービス提供していてありがたい…と思って調べてみたら

この度、生産、販売終了品の旧製品における、Eye-Fi View及び、Eye-Fi Premiumが他社オンラインサービスとの連携につきまして、2015年9月9日をもちまして、終了いたしますことをお知らせいたします。

サービス終了告知が出ていた(終了日時以降も何故か使えてるけど…)、というオチが…。お、おう。

*1:※もし私が運用担当でめんどくさいシステムを押しつけられたらそう思ってしまうかも

*2:※要出典

*3:* Google Photos にアップするだけなので作ってみたとは違いますが…

*4:[permalink が万が一漏れるとアクセスできる可能性はありますが、機密事項を取り扱っていない&キッチンは執務スペース外のため、リスクとして許容してます

*5:なお現在は Azure blob storage をメインのストレージとして使ってます

*6:自家製の Wiki + Blog システムで、JavaScript も書けます


安心してRailsアップグレードを行うための工夫

$
0
0

こんにちは。技術部の国分 (@k0kubun) です。

3/28にクラウドワークスさんで行なわれたRails Upgrade Casual Talksで、Railsアップグレードの際にクックパッドが行なっている工夫について紹介しました。

影響範囲の予測が難しいRailsのアップグレードを安全に行なうための動作確認のやり方について参考になればということで、本記事でも改めて紹介いたします。

CookpadのRailsアップグレードの流れ

Rails 4.1から4.2にアップグレードした際の例を紹介します。

CIにRails 4.2用ジョブを用意

まずはRails 4.2にアップグレードするためのrails42ブランチでテストを通します。リリースするまでこのブランチはmasterからrebaseし続けるので、リリースまでテストを通る状態を保つため、CIにrails42ブランチ用のジョブを用意します。このジョブはCIサーバーのリソースが空いている早朝に実行します。

cherry-pick

テストが通るブランチができたら、デプロイ後の問題の切り分けを容易にしたり、レビューの負担軽減のためPull Requestを分割します。ここでは、例えばRails 4.1, 4.2両方で動く修正やgemのアップグレードを先に行います。

非互換の一時的抑制

Rails 4.2にはMasked Authenticity Tokenという、セッションに後方互換性のない変更を加える機能があります。一度デプロイしてしまうと安易にロールバックできなくなるため、最初のRails 4.2アップグレード時には以下のようなモンキーパッチを行なってリリースしました。

Masked Authenticity Tokenの変更を抑制するパッチ

ActiveSupport.on_load(:action_controller) domoduleActionControllermoduleRequestForgeryProtectionExtensiondefform_authenticity_token
        session[:_csrf_token] ||= SecureRandom.base64(32)
      endendBase.prepend RequestForgeryProtectionExtensionendend

このパッチはRails 4.2のリリース後落ちついてから外しました。@minamijoyoさんの発表にあったbreach-mitigation-railsを使うのも良いかもしれません。

動作確認

複数の部署が関わるアプリケーションであり、私一人では全ての影響範囲の確認が困難なため、関係部署が動作確認できる期間を2週間設けています。後述しますが、これと並行して本番に近い環境でエラーが出るかの確認も行います。

cookpad.com のリリース

直前に動かなくなるリスクを抑えるため前日夜からコードフリーズし、デプロイの影響を小さくするため、午前7時に出社して8時にデプロイを行います。なるべくトラフィックが低い時間帯にデプロイしたいものの、関係者全員に深夜の出社を要求するほどではないためこの時間になっています。

複数ブランチ運用

cookpad.com と同じリポジトリに、一つのMountable Engineを共有する8つのアプリが同居しているため、 cookpad.com をデプロイして終わりではありません。間を開けずにリリースすると問題が起きた時の対応が大変になるので、約1週間かけて段階的にデプロイします。しかし、その間はRails 4.1用のrails41ブランチを作りmasterの変更をバックポートし続けるため、あまり移行期間を長くするとその作業が大変になってしまいます。

デプロイ後の監視

デプロイ後はSentryを使ってエラーの確認を行い、自社製のモニタリングツールを使って各サーバーのレスポンスタイムを監視します。

アップグレードフローのまとめ

  1. テストを通しCIにジョブを用意する
  2. Pull Requestの分割や非互換の抑制により変更の粒度を小さくする
  3. 2週間動作確認
  4. 複数のアプリを1週間かけて段階的にリリースする
  5. デプロイ後、レスポンスタイムとエラーを監視

リリース前の4段階の動作確認

Railsのアップグレードをリリースするまでに以下の4段階の動作確認を行なっています。

1. 開発用の検証環境での確認

開発用のDBを参照する検証環境にRails 4.2のブランチをデプロイします。Railsアップグレード関係なく、もともと任意のブランチをhttps://rails42.staging.~/のような任意のサブドメインにデプロイできるようになっていて、この環境を使って各部署に動作確認を依頼します。

2. 本番環境での手動確認

本番環境のサーバーの一つにRails 4.2のブランチをデプロイし、リバースプロキシの設定を変更して動作確認を行なう人だけがそのサーバーにリクエストを飛ばせるようにして、手動で動作確認を行います。

Webの場合

特別なクッキーを持つ場合のみRails 4.2の環境にプロキシされるようにし、適当にクッキーをセットした上で手動で動作確認を行ないます。

f:id:k0kubun:20160329033708p:plain

モバイルアプリ用APIの場合

特別なリクエストヘッダーを持つ場合のみRails 4.2の環境にプロキシされるようにし、mitmproxyでアプリの全てのリクエストにそのリクエストヘッダーを付加し、手動で動作確認を行ないます。

f:id:k0kubun:20160329033718p:plain

3. Kage

github.com

Kageを使い、あるサーバーに来たリクエストをRails 4.2をデプロイしたものにも流し、そこではDBなどへの書き込みや外部サービスへのリクエストを抑止します。実際の様々なリクエストが流せるため、手動では見つかりにくいエラーを発見したり、パフォーマンスの変化を確認したりすることができます。

f:id:k0kubun:20160329033633p:plain

4. Production Test

Railsアップグレード関係なく、クックパッドでCIが通ったサービスは「Production Test」と呼ばれるほぼ本番の環境に自動的にデプロイされます。リリース直前で再度本番と同じ環境で動作確認をすることで、より安心してデプロイを行なうことができます。

Cookpadが遭遇したRails 4.2のバグ

Rails 4.1 → 4.2のアップグレードではリリース後にエラーが大量に出るようなことはなく、これまでリリース後に発見されたRails自体のバグは以下の2つだけでした。自社のアプリの保守性のためにモンキーパッチを防ぐという意味だけでなく、コミュニティに貢献するという意味でもなるべくOSSのバグは本家にPull Requestを投げて修正するようにしています。

Encoding::UndefinedConversionError

  • マルチバイト文字列かつ半角の"%"を含むファイル名のファイルをアップロードした際に出るレアなエラー
    • Rails 4.1 → 4.2 アップグレード時に唯一出たエラー
  • 本家に @eagletmtがPull Requestを投げ、4.2.4に取り込まれました

undefined method 'unpack' for nil:NilClass

  • Rails 4.2.4とRuby 2.0.0の組み合わせでのみ発生するエラー
    • Ruby 2.0.0だとERB::Util.url_encode内のgsubのブロックで$&が参照されますが、ActiveSupport::SafeBufferだとこれが動きません
  • 本家に @k0kubunがPull Requestを投げ、4.2.5に取り込まれました

まとめ

クックパッドがRailsアップグレードの際に行なっている工夫について紹介しましたがいかがだったでしょうか。このようにしてクックパッドは常に最新のRailsを使い続けており、今も http://cookpad.comはRails 4.2.6で動いています。より開発しやすく、バグや脆弱性の少ない最新のRailsを使い続けることで、より良いサービスを届けられるようにしたいですね。

開発チームと営業チームとのコミュニケーションで気を付けていること

$
0
0

こんにちは。クックパッド特売情報ディレクターの田中です。 本日は、開発チームと営業チームのつなぎ役として働くディレクターとして、コミュニケーションにおいて意識していることをご紹介したいと思います。

特売情報のビジネスモデル

私が担当しているクックパッド特売情報はいわゆるB2B2Cというビジネスモデルになっています。

f:id:yojitanaka:20160330171820j:plain

こちらの図の様にクライアント(小売店)の方からお金をいただく代わりに情報掲載の場とエンドユーザーの方々の関心をデータ化してお返しし、いただいた情報を私達が見やすい形に整えたり、更に付加価値を付けた上でエンドユーザーにご提供しているという形になっています。

B2B2Cサービスにおけるディレクターの役割

クックパッドで「ディレクター」と呼ばれるスタッフの役割は、以前検索・編成部の五十嵐さんがまとめて下さいました

これらの役割をベースとして、特売情報のディレクターはクライアントユーザーとエンドユーザーの双方にメリットを届けられるように、開発チームと営業チームの間に立ち、プロダクトの要件定義と最終的にそれを使ってもらうように働きかけるところまで責任を持っています。

f:id:yojitanaka:20160330171857j:plain

この際、開発チームと営業チームに対してでコミュニケーションの勘所が違うな、と最近思うことが強くなってきました。 少しバイアスがかかっているところもあるかもしれませんが、考えをまとめます。

開発チームとのコミュニケーション

対面の会話を適切なタイミングで

エンジニアのみなさんからすればうっとうしいと思われてしまうかもしれませんが、やはり都度口頭で対話し、ビジュアルやダイアグラムを使いながら議論した方が最終的に良い物ができることが多いと思います。

特に前回のブログでもご紹介したように特売情報ではGitHubの積極活用が進んでおり、それが仇になってissueやpull request上で壮大な議論に広がってしまうことが稀にあります。

過去自分が直面したケースを鑑みると、文面で正しく意味が伝えられていないことに起因するものがほとんどでした。お互いの時間を無駄にしないためにも、意見が割れたり、議論が噛み合っていないことを感じたらまずは口頭で話す習慣が大事だと考えています。

特定的な言葉を使う

口頭・文面での会話両方ですが、できるだけ特定的な言葉を選び、多義的な表現を避けることでシャープに意図を伝えることが重要だと考えています。 特に要件定義や仕様の段階で振れ幅を許容した表現を使ってしまうと、最終的に開発されたものが自分の意図から外れてしまうことも少なくありません。 これを改善するには技術・デザイン両面の素養を身につけるしか無いと思っています。

明文化を諦めない

一つ目の対話の重要性と矛盾するようですが、口頭での議論内容・意思決定のプロセスなどは意識的に文面に残すようにしています。

f:id:yojitanaka:20160330171902p:plain

目的は2つあり、一つ目は自分自身のための思考の再整理、二つ目は未来に周辺箇所を触る別のメンバーまたは自分への伝言です。

特に二つ目に関しては、後々「なんでこうなってんだっけ」と議論が再発すると、とにかくツライことが多いのはみなさんも経験があるのではないでしょうか。

営業チームとのコミュニケーション

一度の入念な説明より複数回のライトな共有

普段、クライアントと向き合うことがメインの業務である営業チームに対して、新しい価値・機能を正しく理解してもらうことは工夫が必要です。

特に開発チームの規模が大きくなり、複数の機能開発が同時並行的に走っている私のチームでは一度に密な説明を行っても、正しく伝わらなかったり、理解が不十分になってしまうことがありました。

そこで、起案された段階から営業チームに進捗を経時的に細かく共有することにしました。 これにより、機能理解が営業チームで進んだのはもちろん、リリース目処がしっかりと握れることで営業のタイミング・クライアントPRを計画的に行える様になってきました。

誰よりもファンになってもらうべくストーリーで語る

B2B2Cというサービスの特質上、クライアントに機能を使ってもらわないとエンドユーザーに価値が届きません。そして、クライアントに機能を使ってもらうためには営業・サポートチームにその機能・価値を十分に理解し、共感してもらう必要があります。そのためにはディレクターがあたかも営業になったつもりで、営業チームにストーリー的に価値を伝えるのが有効だと考えています。

この時は先程の開発チームとのコミュニケーションとは真逆で、要素要素を特定的な言葉で話すのではなく、クライアントのどんな状況で、どんな課題を、どう解決するかをシンプルに伝えることが重要だと考えています。

デモに勝るものなし

機能がクライアントに提供する価値をストーリーで伝え、共感してもらっても、営業チームが使いこなせなければ自信を持ってクライアントに伝えることは出来ません。

最終的な挙動を理解してもらうのはやはりデモに勝るものはありません。ワイヤーフレーム、動的なモック、ステージング環境とプロダクトの具体性が高くなるたびに細かく共有し、営業チームに自分で使うところをイメージしてもらうことが重要だと思います。

特に日々クライアントと接している営業チームの方がクライアント目線での課題発見は得意なことが多く、早期にその声を拾って要件を修正することで手戻りを減らすことが可能です。

まとめとエンジニアのみなさんにお願いしたいこと

今回は、B2B2Cサービスを担当するディレクターとして開発・営業チームそれぞれとのコミュニケーションで意識していることをご紹介しました。

このブログをご覧頂いている多くの方がエンジニアの方だと思うので、今回ご紹介した内容を踏まえて、最後にひとつディレクターとのコミュニケーションにおいてお願いしたいことがあります。

「無理」の理由を教えてほしい

ディレクターはエンジニアリングの知識を付けてエンジニアに歩み寄りたいと考えていますが、知識が及ばないことがほとんどだと思います。

要件の実現がなぜ難しいのかを出来るだけ噛み砕いて伝えてもらうことで、誰が読み返してもわかりやすい形で明文化でき、後々営業チームに伝えるときにも役立ちます。

また、副次的にディレクター自身が技術的な素養を身につける手助けにもなるので、その後の仕様設計の精度を高めることにも貢献するはずです。

最後に

最後になりましたが、特売情報のようなB2B2Cのサービスはステークホルダーが多いからこその難しさと楽しさがあると思います。 同じような境遇にいる方や領域の違うチームとのコミュニケーションに苦労している方のお役に立てれば幸いです。

開発の見積もりとスケジュール管理

$
0
0

こんにちは。会員事業部の丸山です。

エンジニアが開発を開始する時にはタスクの見積もりとスケジュールを作成行って、実装を進めていくと思います。 しかし1ヶ月を超えるような規模の開発をする場合、なかなか予定通りの期日に終わらなかったりすると思います。 そして大抵の場合、増える方向になりますよね。 今回はそういうことにならないために、私が気をつけていること・実践していることをいくつか紹介したいと思います。

見積もりとは

まずは「見積もり」とは何なのかを正しく理解したいと思います。 一般的には「見積もり」=「全タスクとその工数を洗い出す」というものだと思います。 しかしここで以下のことに気をつける必要があります。

見積もりとスケジュールとコミットメントは違う

見積もりとはあるタスクがどれだけの工数(規模)なのかを算出することです。 対して、スケジュールとはあるタスクがどれだけの工期(期間)なのかを算出することです。 この2つを混同してしまうと、正しく仕事をすすめることができません。 例えばタスクAの工数が8時間だったとしても、それを担当する人の稼働が実は1日に4時間しかない場合、工期は2日(16時間)となります。

次にコミットメントは最終的な予算・期間・品質をどういうバランスにするかを決めて、約束をすることです。 たとえば10個のタスクを見積もり、スケジュールを立てたところ、1人で3ヶ月かかることがわかりました。 この話を責任者にすると「2ヶ月で終わるようにしてくれ」という要望があり、見積もりをなんとか捻じ曲げてして、2ヶ月で終わるようにしました。

しかし、本来はコミットメントをどうするか?という話を責任者とする必要があります。 人的リソース(予算)を増やすのか、リリース日(期間)をもう少し遅らせられないか、機能(品質)を削ることができないかなどを話し合うべきです。 コミットメントが変わらないのに、見積もりやスケジュールは変更されません。 これを混同してしまうと、現場も責任者も両方不幸になってしまいます。

「見積もりをしてください」と言われたら

では「見積もりをしてください」と言われたらどうしたらよいのでしょうか? 見積もりを行うには大きく2つのステップがあります。

  1. 作るべきものをタスクに分解する
  2. タスクの規模を算出する

タスクに分解する

まずタスクに分解するステップですが、これは「タスクに分解する」と捉えるよりも「設計をする」と捉えたほうがよいと思います。 つまり見積もりをするときに一番初めに行うことは設計です。作るべきものを設計し、具体化しなければ正しくタスクに分解できません。 そしてこのことから分かるように、見積もりを正確にするには時間をかけるのではなく、より詳細な設計をすべきです。

ここで、一つ問題になるのがどこまで詳細な設計をすべきなのか?ということです。 これを定量化するのは難しいのですが、開発している対象によってだいたい似たパターンがあると思います。 例えばWebサービスを開発しているサービスエンジニアの場合「画面一覧/Controller/Model/View/テストコード/インテグレーションテスト/テーブルスキーマ/マスターデータ/ログ」などに分解することが多いと思います。 他にもインフラや開発基盤系のタスクを分解する必要があるエンジニアの方もいると思います。 このように、どういうタスクに分解していくかは会社・部署・チーム・個人などである程度決まった型(テンプレート)を作っておくと役に立つと思います。

規模の算出

次にそれぞれのタスクの規模を算出していきます。 ここで重要なのは「主観や判断をなるべく少なくして、機械的に算出する」ということです。 もっとも良いとされているのが、「過去の実績を元に似たタスクから算出する」というものです。 しかしそのためにはこれまでの実績が正しく残っている必要がありますし、「似たタスク」というのがあまり無いケースも多いでしょう。 *1

そこで僕が行っているのは全タスクのなかから平均的な難易度のタスクを選び、それを元に各タスクに難易度を振っていくというものです*2。 難易度は「1, 2, 3, 5, 8, 13, 21」*3ぐらいを用意します。 次に基準となるタスクを一つ決めて、そのタスクの難易度を3(もしくは2)として各タスクに難易度を振っていきます。 難易度が13や21のタスクは現時点ではまだ詳細がわからないや調査が必要というものです。 理想的にはこれらは無いほうがよいのですが、現実的には見積もり時にはわからないことは残っていると思います。

すべてのタスクの難易度が決まったら、基準としたタスクにかかる工数(時間)を算出します。 あとは他のタスクにも時間を掛け算していきます。 たとえば基準のタスクが難易度3で6時間となった場合、難易度に2を掛けた値をそれぞれの工数(時間)とします。

ちなみに、見積もりを他のエンジニアにレビューしてもらう場合は、難易度のままでレビューしてもらうと良いと思います。 工数(時間)でレビューしてもらうと、エンジニアのスキル差によって工数の感覚が違ってくる可能性があるためです。 例えばジュニアなエンジニアが見積もった工数とシニアなエンジニアが見積もった工数では違いが出るのは当然ですよね。 その場合、その二人のエンジニアでタスクの工数について議論をしても有意義なものになりにくいというものです。

スケジュールとは

見積もりの項でも触れましたが、スケジュールとはコミットメントを達成するためにどれだけの期間がかかるかを可視化したものです。 そしてスケジュールを作成するために見積もりを使います。つまり見積もりがあれば、スケジュールを作成するのは簡単です。 むしろスケジュールに関しては作成するよりも、管理するほうが重要でかつ難しいと考えています。

ツール

以降で紹介するスケジュール作成・管理にはMacで使えるOmniPlan*4と呼ばれるガントチャートツールを使います。 OmniPlanを使っているのは、以下の様なスケジュール管理には必須な機能が揃っているからです。

  • リソース: 個々人の稼働率を設定できる
  • カレンダー: プロジェクトの休日、個人の休日、時間外稼働を設定できる
  • ガントチャート: 平準化(自動配置)、基準承認(変更管理)、工数と工期の区別、マイルストーンの設定、遅延タスクの分割

ガントチャートツールには他にもいくつかありますが*5、僕が知る限りのツールはすべて機能不足に感じます。 ちなみにガントチャート以外の方法としてバーンダウンチャートも有名です。 僕はガントチャートのほうが好みですが、スケジュールを正しく作成・管理できるなら好きな方法を採用すればよいと思います。

スケジュール作成

見積もりを元に、タスクと工数を入力していきスケジュール(工期)を作成します。 OmniPlanを使えばタスクと工数を入力して、担当者をアサインしたあとに、平準化(自動配置)という機能を使えばいい感じにガントチャートを作ってくれます。 手動でタスクの配置を設定する必要はありません。

スケジュールを作成する時は以下の点に注します。

  • 非稼動時間を設定する
    • 事前にわかっている非稼動時間はあらかじめ設定しておきます
    • たとえば定例会議、休日、祝日、有給などです
  • 稼働率を設定する
    • 会社に来て対象のプロジェクトに使える割合を設定します
    • 他の人から仕事の相談をされたり、PRレビューをしたり、割り込み作業が発生したりするので個人的には70%〜75%を設定しています
    • 他のプロジェクトを掛け持ちしている場合は40%などになるかもしれません

スケジュールの振れ幅と確率の算出

スケジュールを作成する上でもっとも重要なのが、スケジュールの振れ幅と確率を算出することです。 スケジュールの振れ幅とは最短期間と最長期間との幅のことを表し、確率とはその幅のなかで開発が終了する確率分布を表します。

ありがちなスケジュールは「X月Y日に開発が終了する」というシングルポイントスケジュールです。 これは言い換えると「X月Y日に開発が終了する確率は100%」と言っているのと同じです。そんなことはまずありえません。 なので本来なら「M月N日〜X月Y日の間に開発が終了する確率は75%です」や「Q月R日までに開発が終了する確率は50%です」などのはずです。

そこで開発がほぼ確実(90%〜95%)に終了するであろう期間を最短期間と最長期間として設定します。 私は見積もりを元に作成したスケジュールを最短期間とし、最長期間をそれの1.5倍としています。 確率は50%で開発が終了する日を[最短期間 + (最長期間 - 最短期間) * 0.4]、75%で開発が終了する期間を[50%日 + (最長期間 - 最短期間) * 0.12]としています。 この振れ幅と確率の計算式についてはソフトウェア見積り 人月の暗黙知を解き明かすを参照してください*6

スケジュール管理

スケジュールは一度作成するだけでは全く意味がありません。開発が進むに連れて、スケジュールも更新して、予実管理をしていく必要があります。 私はOmniPlanを使ったスケジュール管理では2つのイテレーションを回しています。

  • 毎日の更新
    • タスクの進捗を記入します
      • 進捗は0%(未着手)、25%(着手)、50%(動くものができた)、75%(レビューに出した)、100%(完了)という粒度で行っています
    • 遅延しているタスクをすべて現在日以降に再配置します
      • OmniPlanの遅延タスクの分割機能を使えば自動で可能です
  • 週1回の更新
    • 残りの期間をもとに振れ幅と確率を計算しなおします
    • 先週(先々週)のスケジュールと比較してどれだけズレが発生しているか確認します
      • 定常的に遅れてきているようであれば見積もりから全体を見直す必要があるかもしれません
      • 突発的に遅れた場合は、稼働を調整したり、タスクの組み換えなどで対応する必要があるかもしれません

このようなイテレーションで予実管理を行い、スケジュールが生きた状態を保ち続けることが大事です。

おわりに

以上、私が行っている開発の見積もりとスケジュール管理の紹介でした。 開発の規模、チームの規模、事業のフェーズ、作るものの種類などによって、この方法をそのまま使えるわけではないと思います。 しかし、これを読んでいただいた方にとって何かしら参考にできるところがあれば幸いです。

*1:例えば受託開発でよく似たシステムを何回も開発している場合は過去の実績から似たタスクを探しやすいかもしれません

*2:プランニングポーカーで使われるポイントと同じ意味

*3:フィボナッチ数列

*4:WindowsユーザにはMS Projectが良いと思います

*5:GanttProject, brabio, GANTTplanner, gantter, Instagantt

*6:この本には振れ幅と確率については見積もりの段階で行うと書いてあります。しかし、チームのメンバーが同じ稼働率、同じスキルレベルの場合はスケジュール作成時に振れ幅と確率を計算するほうが簡単です。

Try! Swiftで感じた将来

$
0
0

こんにちは。広告事業部のモバイルエンジニアパヴェウ @RusinPawです。

先日、3月に東京で開催されたtry! Swiftカンファレンスに行ってきました。開催者のおかげで世界中の様々な優秀なスピーカーが発表してくれました。発表内容はSwiftに留まらずiOSフレームワーク(CoreData、CoreAnimation、HomeKit)やテスティング、ユーザビリティ、コードリーダビリティなど、様々でした。

Swiftとは

Swiftとは2014年のWWDCでAppleが公開し、プログラマーコミュニティを驚かせたプログラミング言語です。モダン、安全、書きやすくて読みやすい、既存のObjective-Cのコードベースに同時と使える言語と言われました。 実は「そんなにすごいのか」の議論はまだまだ続いています(SwiftとObjective-Cをミックスする時に困っている場合がある)が、Swiftは次第に多くのiOS開発者に支持されるようになり、新規プロジェクトのスタンダードになりました。2015年のWWDCではAppleはSwiftをオープンソースとして公開し、開発者たちを興奮させました。開発者は、言語開発とその進化の方向に関わることができるようになりました。

Swiftでコードを書くことのメリットは?

Swiftの公開後、「新規プログラミング言語が必要なの?」という疑問が多かったです。Objective-Cで作ってリリースして成功したアプリがたくさんあるからです。ところが、iOSの開発を始めようとする開発者にとってメッセージベースで複雑ワンマンチームの開発者がアプリをObjective-Cで書きリリースするのはかなり難しいです。クライアントとサーバーAPIを共にSwiftで書くことで、アイデアから最小構成の製品を作り上げることが簡単にできるようになると思います。

Objective-Cを書いてる経験者にとってもObjective-Cの複雑で冗長なシンタックスのコードを読んだり、管理したり、デバッグするのは時間がかかります。クックパッドのように長い間プロジェクトを開発し続ける会社の場合は、ソースコードは貴重な資源として扱うので、できるかぎり読みやすいコードを書くのは大事です。

Try! Swift

Try! Swiftの発表の内容は多岐にわたり、言語そのものに関する詳しい話もありました。それらから、Swiftとそのコミュニティが現在直面する大きな希望と挑戦を示すいくつかのメインテーマがみえてきました。

既存のオブジェクト指向プログラミングのフレームワークを使いながら関数型の機能とプロトコル指向機能を使える

Swift自体はマルチパラダイム言語ですが、既存のiOSフレームワークはすべてオブジェクト指向のパラダイムで作られていました。なのでオブジェクト指向の世界でSwiftの機能を使う方法について色々な話がでました:

  • 野中彩花氏は実践的 “Boundaries”コーディネートのパターンで内部クラスと外部のAPIを区別する方法について発表をしました。
  • Daniel Eggert氏は既存のObjective-CのCoreDataのAPIをプロトコルとエクステンションを使ってモダンなのSwiftのインターフェイスを作りました(Modern Core Data)。
  • Daniel H Steinberg氏はBlending Culturesでオブジェクト指向と関数型とプロトコルという3つの世界を一緒に合わせてそれぞれの特徴を使えることについて話しました。
  • Chris Eidhofのライブコーディングセッションでは view controllerからconfigurationオブジェクトとしてカスタム部分を抜きだし、複雑さを下げる方法が紹介されました。Swiftの関数は第一級オブジェクトなのでそういうconfigurationにカスタムロジックを入れることができます。この仕様を作ったら一つの複雑なview controllerではなくて汎用のview controllerとテストしやすい設定を分けることができます。

Cross-platformとオープンソースSwiftの流行り

Swiftのオープンソース化のおかげで一般のエンジニアはその新規言語の方向を決められます(Jesse Squires, Contributing to Open Source Swift)。LinuxでもSwiftの開発ができます。try! Swiftの発表には、サーバーサイドSwiftに関する発表(Soaring Swiftly - Server Side Swift - Caesar Wirth)、Linuxでの開発で起こる問題(Practical cross-platform - JP Simard)についての話もありました。今現在、一番不便なのはObjective-Cのダイナミックランタイム、そしてダイナミックランタイムで使えるキャストかGrand Central Dispatchがないことです。それでcross-platformを作るなら:

#if os(OSX)
    // ダイナミックランタイム機能を使う
#else // Linux
    // なんとかする
#endif

みたいなひどいコードが多くなります。

Swiftでモバイル以外の開発はまだまだなので必要なツールが少ないです。だからプログラミングしやすくできるようにツール、またはCIツールを開発する機会があります(池田翔 - Dive in Swift Ecosystem)。自分で新しいライブラリを立ち上げる(Creating a Swift Library by Jeff Hui)か、それとも他のエンジニアに便利なツールを作るか色々な貢献のチャンスがあります。

クックパッドでSwiftをtry!してますか?

現在、クックパッドの新規アプリはすべてSwiftで書いています:

しかし、メインの「クックパッド レシピ検索&スーパーのチラシアプリ」はほとんどObjective-Cで開発してます。Swiftで書いてるコードはPOCとして実装されたUITestです。

なぜかというと、アプリで使ってるCocoapodsというdependency managerを利用しているからです。CocoapodsとSwiftのターゲットのダイナミックライブラリをiOS8以後で一緒に使う可能です。iOS7でもターゲットのダイナミックライブラリビルドすることができますが、そのようなアプリはAppleの審査でリジェクトされてしまうでしょう。 クックパッドのアプリはまだiOS7をサポートしているので、Swift を利用するかどうか慎重に考えているところです。そのサポートが終了した際には以下の方法で既存のコードベース内のSwiftのソースコードの割合を増やしていこうとおもいます。

  • 新規クラスをSwiftで作ります。
  • 既存のObjective-CクラスにSwiftで作った新規メソッドを追加します。
  • Swiftのextensionを使って既存のObjective-CクラスにSwiftで作った新規メソッドを追加します:
@implementation ClassA

    - (void)methodA { ... }

    - (void)methodB { ... }

@end
extension ClasA {

    func methodC() {

        ...

    }

}

たとえ、我々のようにSwiftを利用できる場面が限定されていても、Swiftを勉強することで、安全で読み易いコードを書けるようになると思います。Swiftの特徴的な機能:

  • 継承よりインターフェイスを使う
  • オブジェクトのステート数を減らす
  • オブジェクトをイミュータブルに強制される

に従えばコードを理解したりデバッグしたりするのに使う時間を減らせると思います。来年のtry! Swiftが会議が楽しみです。そして来年のSwiftとSwift製ソフトウェアがどうなるか気になります。

株主優待を迷わずご利用いただくために気をつけたこと

$
0
0

こんにちは。会員事業部の高田です。

クックパッドでは、昨年に続き今年も株主優待としてプレミアムサービス1年分クーポンを配布しました。

株主の方々は、普段クックパッドを使っているユーザー層より年齢層が高く、インターネットに慣れていない方も多いと想定されたため、迷わずご利用いただけるよう気をつけました。今回はこのような事情から気をつけたことについて、昨年の事例を元に書きます。

一番最初にクーポンコードを入力してもらう

f:id:satoship:20160411144737p:plain:w600

プレミアムサービス1年分クーポンを利用するには、クックパッドにログインしている必要があります。多くのクックパッドのユーザーはログインせずに使っていることと、前述の想定から、株主の方々もログインしていない可能性が高いと考えました。

そこで、ログイン(またはユーザー登録)→クーポン入力という流れにすると、ログインしただけで離脱してしまったり、ログインした後に迷ってしまったりするのではないかと考え、一番最初にクーポンを入力してもらうことにしました。クーポン入力後、ログインしていないユーザーにはログインやユーザー登録の案内を表示し、ログインしているユーザーにはクーポンご利用画面を表示しました(以下のスクリーンショットを参照)。

[ログインやユーザー登録の案内] f:id:satoship:20160411144755p:plain:w600

[クーポンご利用画面] f:id:satoship:20160411144940p:plain:w600

ユーザー登録後にクーポン利用画面へのリンクを表示する

f:id:satoship:20160411145005p:plain

ユーザー登録後にどこでクーポンを利用すればいいのか迷ってしまうのを避けるために、ユーザー登録後の画面でクーポン利用画面に戻るリンクを表示するようにしました。

これは、前述の通り最初にクーポンコードの入力をしてもらっているため、Cookie の値などから、クーポンを利用しようとしているユーザーを判定することで表示しました。

「株主優待」で検索するとリダイレクトするようにする

配布したクーポンには「株主優待ご案内ページ」として URL と QR コードが印刷されていますが、うまく辿りつけずに問い合わせがくるかもしれません。また、サポートスタッフがなんらかの説明をする際に「株主優待ご案内ページ」を表示してもらいたいことがあるかもしれません。

そこでクックパッド内で「株主優待」と検索すると「株主優待ご案内ページ」が表示されるようにしました。

まとめ

今回は、プレミアムサービス1年分クーポンのシステムを開発する際に、迷わず利用できるように気をつけたことについて書きました。

はじめての試みだったため、明確な効果をお伝えすることはできませんが、開発チームの予想を上回る株主の方にご利用いただくことができました。また、問い合わせの件数も予想より少なく無事に利用期間を終えることができました。

サポートスタッフからは、実際のお問い合わせの際には、クックパッドのトップページを開いている場合が多かったため、「株主優待」と検索するよう案内できるのは便利だったというフィードバックもありました。

どの事例もささいなことですが、ディレクターやサポートスタッフや開発者がこれなら迷いにくく、サポートしやすいだろうという案を出し合って決めました。参考になることがあれば幸いです。

オープンソースソフトウェアポリシーをつくろう

$
0
0

こんにちは、みんなのウェディング 高井です。

みんなのウェディングやクックパッドといったインターネットサービス企業では、オープンソースソフトウェアは欠かすことのできない存在です。LinuxやMySQL、Ruby、Railsといった主要なものをはじめとして、テクノロジースタックのほとんどがオープンソースソフトウェアによって構成されいるといっても過言ではありません。

ですから、企業としてどのようにオープンソースソフトウェアに向きあうかということが、とても重要な問題になります。そして、そのための指針が、オープンソースソフトウェアポリシーです。

今回は、クックパッドがどのようにオープンソースソフトウェアポリシーをつくったか、その背景も含めてをご紹介いたします。

クックパッドとオープンソース

今でこそクックパッドは、多くのオープンソースソフトウェアを公開したり、その開発に貢献したりする会社となっています。しかし、私が入社した5年前には、会社として公開しているオープンソースはひとつもありませんでした。入社したばかりのときに「ちょっとしたライブラリをオープンソースとして公開したい」と当時のCTOである橋本に相談したときに、なぜそのようなことをするのか説明を求められて驚いた記憶があります。個人の活動としてオープンソースを公開している社員はいたのですが、企業の活動としてオープンソースソフトウェアに取り組む文化はありませんでした。

そこで、私はオープンソースソフトウェアポリシーの素案をつくり、会社に提案をしました。その素案はいくつかのフィードバックをもとに修正されたのち、正式に承認されたものになっています。

また、みんなのウェディングで仕事をするようになって、まず最初にやったのがオープンソースソフトウェアポリシーを策定することでした。その結果、みんなのウェディングでもオープンソースソフトウェアを公開したり、その活動をブログで紹介したりする成果につながっています。

なぜオープンソースソフトウェアが重要か

そもそも、なぜ企業としてオープンソースソフトウェアにコミットしなければならないのでしょうか。

第一に、私たちのようなインターネットサービス企業ではその技術基盤の大部分をオープンソースに依存しているという現実があります。かつて、企業の技術基盤は、大手ベンダーが開発するプロプライエタリ・ソフトウェアが中心となっていました。ごく初期のインターネット企業にとって、サン・マイクロシステムズのSolarisやオラクルのOracle Databaseは間違いのない選択肢でした。また、初期のクックパッドが、ColdFusionで開発されていたというのは有名な話です。そのような時代には、企業はベンダーのロードマップに従っていれば何の問題もありませんでした。

ところが、その状況が変化していきます。エリック・レイモンドが「伽藍とバザール」で示したようなバザール式の開発モデルが一般的となり、それが優れたモデルとして受け入れられていきました。バザールプロジェクトでは、その開発に誰もが自由に参加することができます。だからこそ、自分たちが利用しているプロダクトの継続的な進化や発展のために、そのプロジェクトにコミットしてくことが重要になってきたのです。

第二に、人材採用面でのメリットです。オープンソースプロジェクトには、技術力の高いエンジニアが多く参加しています。会社がオープンソースコミュニティの一員になることでそうした人材を獲得するチャンスが増えます。

また、それが企業内にオープンソースプロジェクトでデファクト・スタンダードになっている開発スタイルを取り入れ、健全な開発者文化をつくり上げる基礎となります。オープンでフラットな意思決定、具体性がない議論よりも動くコード、そういったものをエンジニアは好みます。

職務著作とオープンソース

企業にオープンソースソフトウェアへのコミットメントをしていくとしたら、整理しなければならないことのひとつに、著作権があります。著作権法では、仕事で作成するプログラムについて、次のように定められています。

第十五条

2  法人等の発意に基づきその法人等の業務に従事する者が職務上作成するプログラムの著作物の著作者は、その作成の時における契約、勤務規則その他に別段の定めがない限り、その法人等とする。

つまり、仕事でプログラムを作成したときには、原則として著作権は会社に所属することになります。もし、あるエンジニアが会社で利用しているオープンソースソフトウェアにバグを発見して、就業時間中にその修正パッチを作成したとします。そのパッチの著作権は会社に所属することになりますので、そのエンジニアの判断で勝手に公開してしまうと、著作権上問題のある行為となってしまいます。

ですから、企業としてオープンソースソフトウェアへの貢献していくためには、著作権の扱いをどうするか整理をする必要があります。そのための仕組みがオープンソースソフトウェアポリシーです。

オープンソースソフトウェアポリシーがなくても、暗黙的にうまくやっているという会社もあるでしょう。しかし、著作権の問題を考えるとあまり良いやり方であるとは思えません。オープンソースソフトウェアポリシーは、そこで働く開発者の行動を制限するものではなく、守るためのものです。

オープンソースソフトウェアポリシーのつくり方

では、オープンソースソフトウェアポリシーをつくりたいときに、どのようにすればいいでしょうか。

クックパッドのオープンソースソフトウェアポリシーは、一般社団法人情報サービス産業協会(JISA)がまとめた「オープンソースビジネスに取り組むSI企業のための企業ポリシー策定ガイドライン」を参考につくられました。「SI企業のための」となっていますが、オープンソースソフトウェアポリシーを策定するにあたって検討すべき項目が網羅されていますので、たいへん参考になります。

そのうえで、自分たちの実現したいことをきちんと議論し、その議論を反映させたものにすることも大切です。テンプレートのようなものがあったとしても、それを持ってくるだけでは自分たちの会社の理念に沿ったものにはなりません。ポリシーなのですから、それを自分たちの言葉で説明できなくては、価値あるものとして機能しないでしょう。

まとめ

クックパッドでは、ポリシーを定めることで、従業員が自由にオープンソースソフトウェアに貢献することのできる土壌をつくっています。また、そのことが優秀な人材を獲得し、健全な開発者文化を醸成することに貢献してきました。

最後に宣伝となりますが、みんなのウェディングはソフトウェアエンジニアを募集中です。「みんなの『大切な日』をふやす」を経営理念にユーザーファーストなサービスづくりを徹底的にやっていきたいと考えている会社です。ご興味のある方は、 株式会社みんなのウェディング採用情報からご応募ください。また、みんなのウェディングのブログもあります。こちらではエンジニアリングに関する話題をあつかっていますので、あわせてご覧ください。

どのようにして高速に iOS アプリの UI を作り上げるか:動作モックの活用と実装時の UI 作りこみ

$
0
0

Holidayデザイナーの多田です。

皆さんは Web アプリやモバイルアプリを開発する時、モックアップ作成にどれだけ時間を割いているでしょうか?もしくはモックアップを作成せずにすぐに実装に入るでしょうか?私はこれまで Web アプリ開発ではいきなり実装に入ることが多かったのですが、Holiday iOS アプリの開発では Web アプリの時のように上手くいかないことに気づき、やり方を考え直しました。iOS アプリ開発の過程で、モックアップ作成や実装をどのように捉えるか、具体的にどう行うか、ということが自分なりに見えてきたので、それらについてご紹介します。

目的は、価値のあるプロダクトを速くユーザの手に届けること

Web アプリやモバイルアプリの開発過程においてモックアップなどを作る目的は、あくまでも ユーザに届くプロダクトの価値を高めてそれを速くリリースすることです。適切な前準備は、やろうとしていることが価値があるのかどうか、またその価値が伝わるのかどうか、ということに実装前の早い段階で気づくことができるというメリットがあります。重要なことは、 モックアップや実際のプロダクトなどを作ることによって確かめられる仮説検証の精度と、それを作る時間はトレードオフの関係にあるということです。そのため、モックアップなどを作る際には、本当に作るべきなのか、もしくはどこまで作りこむのかということを意識する必要があります。

Web アプリとモバイルアプリの実装コストの違い

Web アプリとモバイルアプリの開発をどちらもやっていて気づいたことは、ほとんどの場合、モバイルアプリは Web アプリよりも UI の実装や調整に時間がかかるということです。主な理由としては、モバイルアプリが下記のような特性を持つからだと考えられます。

  • コード記述量が多くなったり、UI 絡みでクラッシュしやすい
  • ビルドに時間がかかる
  • ユーザが UI に求める期待値が高いので、最初からある程度作りこむ必要がある
    • リッチなインタラクション
    • スムーズな動作

実装コストが高いということは、それだけ失敗した時のダメージが大きいということになるため、かける時間と得られるもののバランスが取れている(いわゆるコストパフォーマンスの良い)方法がないか考えたほうがよさそうです。よく用いられる方法として、

  • スケッチ&ペーパーモック
  • 静止モック(Sketch など)
  • 動作モック(Flinto for Mac, Framer, InVision など)
  • 実装

というものが挙げられます。これらによって得られる検証精度と作成にかかる時間の関係は以下のようなイメージです。

f:id:tdksk:20160420192614p:plain

上の図のように、Web アプリと比べモバイルアプリは実装に時間がかかるため、 動作モックを活用することと 実装時に UI の作りこみをしやすくすることを特に意識しています。これらについて、具体的にどのようなことを行っているのか説明します。

動作モックを活用する

f:id:tdksk:20160421003428p:plain

先述のとおり、モバイルアプリは実装フェーズで一気にコストが上がるため、途中の穴を埋めることには価値があると考えられます。最近では例えば以下のように様々なプロトタイピングツールが登場しています*1

プロトタイプの精度と作成にかかる時間のバランスを考えた時、個人的に一番しっくりきて活用しているのが Flinto for Macです。作成可能なプロトタイプの幅が多く、かつプロトタイプを作るエディタが非常に使いやすいため、多くの場合で十分な精度のプロトタイプを素早く作ることができます。

Holiday iOS アプリの開発では、作る機能ごとに必要に応じて Sketch で画面を作り、その画面のパーツを使って Flinto for Mac でプロトタイプを作成しています。実装前にリアルなインタラクションを気軽に何パターンも試すことができるため、頭の中のイメージやラフスケッチから実装に入ったり、Sketch でモックを作成したあとすぐに実装に入るよりも手戻りが少なくなり、結果としてかかる合計の時間が短くなる場合が多いです。

f:id:tdksk:20160420192642g:plain

実装時に UI の作りこみをしやすくする

モバイルアプリはビルドに時間がかかるため、実装時に UI の作りこみをしにくいと感じることが多いです。逆に言えば、実装時に UI の作りこみがしやすければ、モック時点で作りこむ必要が薄れるため、Web アプリのように早い段階で実装に入って時間を削減できる可能性があります。

f:id:tdksk:20160420192633p:plain

Holiday iOS アプリでは、Tweaksというライブラリを使うことによって UI のパラメータをビルド後のアプリ上で動的に変更しています。このライブラリを使うと、以下のように動的に変更したいパラメータの箇所を一行変えるだけで、アプリ上でパラメータ設定画面(振ると出てくる設定画面を最初に数行書いておくだけで作ることができる)から値を変更できるようになります。

NSUInteger width = FBTweakValue(@"Activity", @"Spot", @"Width", 240);

f:id:tdksk:20160420192747g:plain

このライブラリは他にも以下のような機能を備えており、実用性が高いです。

  • 数値だけでなく、文字列や BOOL 値、また色などもアプリ上で設定可能
  • 数値の下限と上限を設定することが可能
  • マクロを使っていてリリースビルドではデフォルト値を展開するだけなので、パフォーマンス上問題にならない
  • 値が変更されたら自動で更新するビュー(オブジェクト)を設定することができる
  • 値の設定画面上でリストが選択された時に実行するブロックを加えることができる
  • 設定画面の呼び出しを手動で定義することもできる

ただし、先述のコードは Objective-C でマクロを使っているため、Swift ではそのままでは同じように一行で書くことができません。Swift の場合は Facebook Tweaks with Swift Tutorialの記事に書いてある方法でヘルパーメソッドを作ると、Objective-C での場合と似たように一行で記述することができます。

まとめ

今回は、iOS アプリ開発において UI を素早く作り上げるために Web アプリとは異なるやり方、具体的には動作モックの活用と実装時の UI 作りこみをやりやすくする方法を紹介しました。紹介したようなプロトタイピングツールやライブラリ等を用いることによって、iOS アプリ開発全体のワークフローを効率化できると実感しています。実際にリリースされている iOS アプリが気になった方は、こちらよりダウンロードしてぜひ触ってみてください。


オープンソースライセンスの管理を楽にする -Android アプリ編

$
0
0

こんにちは、投稿推進部の吉田です。
オープンソースライセンスの管理はアプリ開発における悩み事の一つですよね。今回はこの煩雑な作業をgradleプラグインを使って自動化する話をします。 本稿におけるライセンスの管理とは、OSSライブラリの著作権者とライセンス文の管理に限定されることを予めご了承下さい。

紹介するgradleプラグイン

license-tools-pluginが提供する機能

  • yamlを使ったオープンソースライセンスの管理
  • ライセンス追記漏れのチェック
  • ライセンス一覧のhtmlの作成

license-tools-pluginの利用方法

複雑な設定は必要なく、3ステップでライセンス一覧を管理することが出来ます。

  1. プロジェクトにlicense-tools-pluginプラグインを導入する
  2. build.gradleから依存ライブラリ情報を抜き出してyamlファイルを作成
  3. yamlからライセンス一覧のhtml作成

導入後はPull Request毎にCIでyamlファイルが適切な状態かチェックすると良さそうですね。

1.プラグインの導入

build.gradleに以下の内容を記述します。

apply plugin: 'com.cookpad.android.licensetools'

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    // 2016-04-28時点の最新バージョンは0.12ですが、最新のものを利用して下さい
    classpath 'com.cookpad.android.licensetools:license-tools-plugin:0.12.0'
  }
}

license-tools-pluginはgradle ver.2.10以上を要求するので、古いバージョンを利用している場合は、gradle/wrapper/gradle-wrapper.propertiesのdistributionUrlを更新します。

distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip

導入手順は以上です。

2.yamlファイルの作成

license-tools-pluginにはyamlファイルを生成するためのコマンドはありません。 代わりにcheckLicensesタスクを実行することで既存のyamlファイルとの差分を出力出来ます。 初回利用時はcheckLicensesタスクが全てのライセンス情報をyaml形式でコンソールに出力するので、 app/licenses.ymlというファイルを作成して出力内容をコピペします。

- artifact: com.squareup.okhttp:okhttp:+
  name: OkHttp
  copyrightHolder: #COPYRIGHT_HOLDER#license: #LICENSE#- artifact: com.squareup.okio:okio:+
  name: Okio
  copyrightHolder: #COPYRIGHT_HOLDER#license: #LICENSE#

出力されたyamlでは、情報を取得できない部分がプレースホルダになるので適宜修正して下さい。上記のyamlの場合は#COPYRIGHT_HOLDER##LICENSE#の部分を調べて手作業で修正する事になります。
ここまでの作業が完了したら、もう一度checkLicensesを実行しましょう。先程は失敗したタスクが正常に終了するようになるはずです。 また社内ライブラリやGoole Play Servicesなど一覧に表示する必要のないライセンス情報はskip: trueで一覧から除外することが出来ます。

- artifact: com.google.android.gms:play-services-basement:+
  name: play-services-basement
  skip:true

3.ライセンス一覧の作成

ライセンスの表示方法はいくつか考えられます。例えばyamlを直接読みListViewなどのネイティブUIで表示する事も可能です。 この場合、license-tools-pluginはUIパーツの提供はしていないので、他のライブラリを利用するか自分で実装することになります。
UIパーツを提供しない代わりにgenerateLicensePageというタスクが用意されていて、yamlからフォーマットされたhtmlを作成する事が可能です。 一例としてgenerateLicensePageで生成したhtmlとwebviewを利用したサンプルコードを貼りました。下のスクリーンショットのようなスタイルのhtmlが表示されていれば成功です。

@Overrideprotectedvoid onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstancestate);
  setContentView(R.layout.activity_main);
  WebView webview = findViewById(R.id.web_view);
  webView.loadUrl("file:///android_asset/licenses.html");
}

おわりに

license-tools-pluginはまだリリースされたばかりで機能はそれほど多くないですが、ライブラリのライセンス管理を劇的に楽にすることが出来ます。 Cookpadではこのようなチョットしたものでもライブラリ化して公開するようにしています。Githubのレポジトリ上でIssueやPull Requestをいつでもお待ちしているので気軽に意見をお聞かせ下さい。

iOSアプリケーションの国際化と地域化

$
0
0

 海外事業向けのiOSアプリケーション開発を担当している西山(@yuseinishiyama)です。クックパッドは現在、海外複数カ国に向けてサービスを展開しています。

 海外事業向けのiOSアプリケーションは、英語、スペイン語、インドネシア語、タイ語、ベトナム語、アラビア語をサポートしています。今後、サポートする言語は更に増えていく予定です。

 これまで、複数の言語に対応するための国際化(internationalization)と地域化(localization)を行ってきました。ここでは、その中で得た知見を以下の4つのパートに分けて共有したいと思います。

  • コンテンツとUIの言語の決定
  • RTL対応
  • 翻訳フロー
  • 翻訳に関するTips

 ちなみに、当該プロジェクトがサポートしているiOSバージョンはiOS8以上です。そのため、iOS9以降でしかサポートされない機能については触れません。

 また、我々のサービスの性質上、精度の高いローカライゼーションを実現する必要があり、標準的な方法に背いた強引な方法を取っている箇所がいくつかあります。必ずしも全てのサービスで同様の対応が必要とは思いません。あくまで1つの事例として参考にしてください。

 以下、「Cookpadアプリ」は海外事業向けのアプリケーションのことを指します。

コンテンツとUIの言語の決定

 本節では、どのような思想に基いて、CookpadアプリがコンテンツとUIの言語を決定しているかについて説明します。

Cookpadアプリおける「言語」の意味

 Cookpadアプリにおいて、「言語」は以下の2つの意味を持ちます。

  • コンテンツの言語
  • UIの言語

 UIの言語は基本的には翻訳に関わる問題です。一方で、コンテンツの言語はそうではありません。それぞれのレシピは、特定の言語圏のユーザーがその言語で投稿したレシピであるため、言語によってコンテンツそのものが異なります。スペイン語圏のユーザーがインドネシア語のコンテンツを見たとしても、ほとんど意味を成しません。理解できないだけでなく、地域特有の材料などが手に入らないため、内容も役に立たないからです。次の画像は、あるインドネシア語のレシピをアプリで表示している様子です。

f:id:yuseinishiyama:20160510150316p:plain

 本節では、Cookpadアプリがどのようにコンテンツの言語とUIの言語を決定しているかについて説明します。

コンテンツの言語を決定する

 現在のCookpadアプリでは1つのアカウントは1つのコンテンツの言語に結びつく仕様となっています*1。これは、前述の理由から、1人のユーザーが複数の言語を跨いでコンテンツを閲覧する可能性は低いと考えているためです。そのため、適切なコンテンツの言語が最初に決定される必要があります。

 本項では、いかにしてコンテンツの言語を決定するかについて説明します。

デバイスの言語やリージョンの設定は信頼できない

 適切なコンテンツの言語を選択する上で、まず参考になりそうなのが言語やリージョンの設定です。言語やリージョンの設定とは、設定のGeneral>Language & Regionからアクセスできる項目のことを指します。

f:id:yuseinishiyama:20160510150138p:plain

 アプリで使用される言語は以下の手順で決定されます。

  1. Preferred Languagesの先頭の言語にアプリが対応しているか調べる。対応していれば、その言語を使用する。
  2. 対応していなければ、Preferred Languages内の次の言語にアプリが対応しているか調べる。対応していれば、その言語を利用する。これを、対応している言語が見つかるまで繰り返す。
  3. Preferred Languages内にアプリがサポートしている言語が見つからなければ、アプリのデフォルトの言語が使用される。

 これらの値は、コード上からも取得可能です、次のコードではPreferred Languagesの先頭の言語を取得しています。ちなみに、NSLocale.preferredLanguages()が返す値は、iOS9から変更されました。iOS8までは、["en"]のような値を返していましたが、iOS9からは、["en-GB"]というように言語とリージョンの組み合わせを返却するようになりました。

letmostPreferredLanguage= NSLocale.preferredLanguages().first?.componentsSeparatedByString("-").first

 ところで、言語やリージョンの設定の値は信用できるものなのでしょうか。全てのユーザーが自分にとって適切な値に設定しているのでしょうか。

 各地のメンバーと調査した結果、言語やリージョンの設定があまりあてにならないということが分かりました。例えば、インドネシアでは多くのユーザーが言語設定を英語のまま使っていることが判明しました*2。端末の流通経路によっては、デフォルトの言語やリージョンの設定が適切とは限らず、また、それらの変更方法を知らないユーザーも多くいるためでしょう。

 そのため、コンテンツの言語の選択にデバイスの言語やリージョンの設定をそのまま適用することはせず、ユーザーがアプリ内でコンテンツの言語を明示的に選択する仕様となりました。

 次のgifアニメはアプリ内で言語を選択している様子です。選択した言語にあわせて、UIの言語やレイアウトも動的に変更されています。例えば、アラビア語選択時は戻るボタンの位置が右側になっています。これらの挙動の実現方法については後述します。

f:id:yuseinishiyama:20160510150319g:plain

コンテンツの言語を予測する

 ユーザーが任意で言語を選択できるとはいえ、誤った言語が選択されないように配慮するべきです。そこで、適切なコンテンツの言語を予測し、それがデフォルトで選択されるようにしました。

 先述の通り、デバイスの言語設定はあまり信頼できるものではありません。一方で、SIMから取得できる国情報はより信頼できるものであるはずです。そこで、コンテンツの言語の予測には、MCC(Mobile Country Code)から得られる国コードを、言語設定や地域設定より優先して使用するようにしました。

 次の例では、SIMから国コードを取得しています。取得した値は、ISO 3166-1で表現された文字列です。

import Foundation
import CoreTelephony

structNetworkInfoUtils {
    staticfuncisoCountryCode() ->String? {
        letnetworkInfo= CTTelephonyNetworkInfo()
        letprovider= networkInfo.subscriberCellularProvider
        return provider?.isoCountryCode // ISO 3166-1
    }
}

 この値が取得できなかった時に、初めて言語設定や地域設定を参照します。次のCountryCodePredictor型のinferredISOCountryCode()メソッドは、予測された国コードを返却しますが、まずSIMから得た国コードを参照し、次に地域設定から取得できる国コードを参照しています。

import Foundation

structCountryCodePredictor {
    staticfuncinferredISOCountryCode() ->String? {
        return countryCodeFromNetwork() ?? countryCodeFromLocale() ?? nil
    }

    privatestaticfunccountryCodeFromNetwork() ->String? {
        return NetworkInfoUtils.isoCountryCode()
    }

    privatestaticfunccountryCodeFromLocale() ->String? {
        letlocale= NSLocale.currentLocale()
        return locale.objectForKey(NSLocaleCountryCode) as? String
    }
}

地域に応じてコンテンツを最適化する

 ちなみに、取得した国コードはヘッダを通じてAPIにも送っています。これは地域によって、検索結果などを最適化するためです。

 例えば、同じスペイン語圏であっても、スペインとメキシコではもちろん料理が異なります。アクセス先の地域の料理が優先的に表示されるようにしています。次の画像のうち、1つ目はスペインからの、2つ目はメキシコからの検索結果の例です。

f:id:yuseinishiyama:20160510150507p:plainf:id:yuseinishiyama:20160510150508p:plain

UIの言語を決定する

 既に述べたとおり、コンテンツの言語とUIの言語は別の概念です。そのため、コンテンツとUIで別の言語を使用することも可能です。しかし、Cookpadアプリではユーザーが選んだコンテンツの言語にUIの言語も合わせるという方針を取ることにしました。

 本項では、そのような方針に至るまでの経緯と、その実現方法について説明します。

UIの言語をコンテンツの言語に一致させる

 UIの言語を決定する上で、Appleの思想に則った最も適切な方針はデバイスの言語設定を尊重することでしょう。先ほどの、言語とリージョンの設定画面にも次のような説明文があります。

Apps and website will use the first language in this list that they support.

f:id:yuseinishiyama:20160510150317p:plain

 しかし、既に述べた通り、デバイスの言語設定は適切とは限りません。もちろん、母国語でない言語を理解できるユーザーや、言語が理解できなくとも操作方法を推察することができるユーザーも多数存在するでしょう。しかし、「世界中の人々に向けて毎日の料理を楽しみにするサービスを提供していく」という我々のミッションの性質上、このようなユーザー以外の人々の課題も解決しなければいけません。UIの言語もユーザーが「確実に」理解できるものであるべきです。

 ここまでで説明したように、コンテンツの言語はユーザーが明示的に選択しているため、ユーザーが理解可能な言語であることはほぼ間違いありません。そこで、UIの言語をコンテンツの言語と一致させることにしました。

UIの言語を動的に切り替える

 ローカライズされた文字列を取得するには、NSLocalizedString(_:comment:)関数を使用します。第1引数に指定したキーに対応する、ローカライズされた文字列を取得することができます。

NSLocalizedString("key", comment:"comment")

しかし、NSLocalizedString(_:comment:)関数はデバイスの言語設定を参照して、どの言語のリソースファイルを使用するかを決定するため、アプリ内で言語スイッチを持つ場合に、これをそのまま使用することはできません。

 あり得る手段の1つとしては、実行時にアプリの言語を強制的に上書きするというものです。

NSUserDefaults.standardUserDefaults().setObject(["es"], forKey:"AppleLanguages")
NSUserDefaults.standardUserDefaults().synchronize()

 しかし、この方法にはアプリの再起動が必要であるという致命的な欠点があります。WWDC14のセッション"Advanced Topics in Internationalization"でも次のように説明されています。

Changing the language preference requires restarting apps

 利用開始後、早々にアプリから再起動を促されるという体験はユーザーに与える印象をかなり悪くすると思われます。そこで、NSLocalizedString(_:comment:)関数をラップした、任意のリソースファイルから文字列を取得する関数を実装しました。ここで、FoundationNSLocalizedString(_:comment:)関数と重複するにも関わらず、アプリ本体の名前空間に同名の関数を定義している理由は後述します。

funcNSLocalizedString(key:String,
                       tableName:String? = nil,
                       bundle:NSBundle= NSBundle.mainBundle(),
                       value:String="",
                       comment:String) ->String {
    varbundleToUse= bundle
    ifletlanguageCode= (ユーザーが選択した言語の言語コード)
        letpath= bundle.pathForResource(
            languageCode,
            ofType:"lproj"),
        letbundle= NSBundle(path:path) {
            bundleToUse = bundle
    }

    return Foundation.NSLocalizedString(key,
                                        tableName:tableName,
                                        bundle:bundleToUse,
                                        value:value,
                                        comment:comment)
}

 ところで、このようにリソースファイルへのパスをアプリ内で組み立てることをAppleは推奨していません。WWDC15のセッション"What's New in Internationalization"では次のように説明されています。

Don’t access the directories yourself

 同様のことを行う場合は、非推奨の方法であるということに留意しましょう。

RTL対応

 本節では、CookpadアプリのRTL対応について説明します。

RTLとは

 アラビア語やヘブライ語のような言語が採用している、右から左へ書くシステムのことをRTL(Right to Left)といいます。ちなみに、英語のような、左から右へ書くシステムはLTR(Left to Right)です。

 RTLの言語を母語とするユーザーは5億人ほどいると言われています。そのためアプリケーションをRTLに対応させることは、ユーザーの獲得という観点から見て非常に重要です。Cookpadアプリはアラビア語に対応しているので、RTLの対応は必須でした。

AutoLayoutによるRTL対応

 AutoLayoutを利用していれば、RTLに対応することは難しくありません。制約を設定する際のNSLayoutAttribute型の値として、.Left.Rightではなく、.Leading.Trailingを指定するだけで大部分がRTLに対応済みとなります。LTR環境において、.Leadingは左を、.Trailingは右を指しますが、RTL環境では、.Leadingが右を、.Trailingが左を指すようになります。

 GUI上で、.Leading.Trailingを指定する場合は、Respect language directionという項目にチェックをいれます。

f:id:yuseinishiyama:20160510150416p:plain

手動でRTLに対応する

 AutoLayoutはデバイスの言語設定を参照してレイアウトの方向を決定します。つまり、AutoLayoutによるRTL対応は、デバイスの言語がRTLでなければ機能しません。

 先述の通り、Cookpadアプリは言語スイッチをアプリ内に持っているため、デバイスの言語はRTLではないが、ユーザーが選択した言語はRTLであるという可能性があります。結果として、AutoLayoutに頼らない手動のRTLを実装することになりました。

デバイスの言語
RTLLTR
選択された言語RTLRTL(AutoLayout)RTL(手動)
LTRRTL(AutoLayout)LTR

 デバイスの言語を明示的にRTLにしているユーザーが、LTRの言語を選択するのはレアケースと考えています。そのため、今のところ、RTLを強制的にLTRにすることはしていません。

要素を反転させる

 手動でRTL対応を行うため、UIViewクラスのtransformプロパティを使用して、ビューを反転させることにしました。以下のようにすると、ビューは左右反転します。UIViewControllerクラスのviewプロパティの値に対して同様の操作を行えば、画面上の要素の左右の順序が入れ替わります。

transform = CGAffineTransformMakeScale(-1, 1)

 次の画像のうち、1つ目はオリジナルのもの、2つ目はルートビューを反転させたものです。レイアウトの方向がRTLになっていることが分かります。

f:id:yuseinishiyama:20160510150311p:plain

f:id:yuseinishiyama:20160510150314p:plain

 しかし、画像を見ても明らかな通り、テキストや画像も左右に反転してしまいます。これらの要素は、個別に、もう一度反転させることで適切な状態になります。

f:id:yuseinishiyama:20160510150315p:plain

翻訳フロー

 機能追加毎に6つの言語への翻訳を行うコストは決して少なくはありません。適切な翻訳フローを設けなければ、翻訳のためのタスクがブロックとなり、スムーズな開発を妨げる可能性があります。

 本節では、Cookpadアプリの翻訳がどのようなフローに則って行われているかについて説明します。

ブランチ戦略

 翻訳は各地のメンバーが行っています。PR毎に全ての翻訳を完了させるということも可能ですが、時差の都合上、翻訳待ちの状態のPRが多くなりがちです。そこで、各トピックブランチでは英語の文言を設定するだけにしました。developブランチには英語の文言だけが存在する変更がマージされ、リリースブランチ上で残りの言語の翻訳を行います。

 このようなフローを取るためにブランチモデルは、git-flowを採用しました。

リリースブランチでの翻訳作業

 本項では、リリースブランチでの翻訳作業について説明します。

エクスポート

 まず、ベースとなる言語(ここでは英語)のXLIFF(XML Localisation Interchange File Format)ファイルをエクスポートします。XLIFFはローカライゼーションのための標準規格で、Xcode 6からサポートされるようになりました。

<?xml version="1.0" encoding="UTF-8" standalone="no"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"version="1.2"xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"><file original="Global/SupportingFiles/en.lproj/Localizable.strings"source-language="en"datatype="plaintext"><header><tool tool-id="com.apple.dt.xcode"tool-name="Xcode"tool-version="7.3"build-num="7D175"/></header><body><trans-unit id="application_config.new_version_available.button_title"><source>Open in App Store</source><note>No comment provided by engineer.</note></trans-unit><trans-unit id="application_config.new_version_available.message"><source>A new version is available in the App Store. Please update before continuing to use the app.</source><note>No comment provided by engineer.</note><note>No comment provided by engineer.</note></trans-unit>(省略)
    </body></file></xliff>

 このファイルはGUI上からもエクスポートすることができますが、xcodebuildを利用すれば、コマンドラインからもエクスポート可能です。自動化のタスクに組み込むなどすると良いでしょう。

xcodebuild -exportLocalizations-localizationPath<エクスポート先>-project<プロジェクト名>

 ちなみに、エクスポートする際に、Xcodeはソースコードを解析して、存在するNSLocalizedString(_:comment:)のキーを確認します。この解析時に、XcodeはNSLocalizedStringという文字列を探して、その後に続く文字列リテラルからキーを取得しているようです。先ほど、NSLocalizedString(_:comment:)のラッパーの関数名に、そのままNSLocalizedStringを使っていたのはこのためです。もし、次のように別名の関数を用意していた場合、エクスポートは上手く機能しません。

funcMyLocalizedString(key:String, comment:String) ->String {
    return NSLocalizedString(key, comment:comment)
}

MyLocalizedString("key", comment:"comment")

OneSkyを用いた翻訳

 このプロジェクトでは、全プラットフォームで翻訳作業にOneSkyというサービスを利用しています。OneSkyはXLIFFフォーマットをサポートしているため、OneSkyに上記の手順で生成したベースとなる言語ののXLIFFファイルをアップロードすることができます。アップロード作業も公式のクライアントを用いれば、コマンドライン上から行うことができます。

 アップロードすると、文言が翻訳可能な状態となるので、後は各地のメンバーに翻訳作業を依頼します。OneSky内で翻訳について翻訳者と議論したり、別のプラットフォームで使用している文言を参照したり、翻訳の進捗状況を視覚的に確認することもできます。

f:id:yuseinishiyama:20160510150415p:plain

 OneSky内で翻訳を外注することもできますが、我々はあくまでツールとして利用し、翻訳自体は各地のメンバーが行っています。単なる翻訳ではなく、サービスの思想を汲んだ上で適切な意訳を行うことができるということが、翻訳を外注しないことの最大のメリットでしょう。

インポート

 翻訳が完了したら、OneSkyから全ての言語用のXLIFFファイルをダウンロードしてプロジェクトにインポートします。Cookpadアプリの場合は、6つの言語をサポートしているので、6つのXLIFFファイルをインポートすることになります。

 エクスポートと同じく、インポートもGUI、コマンドラインの両方から行えます。

xcodebuild -importLocalizations-localizationPath<インポート対象>-project<プロジェクト名>

翻訳に関するTips

 本節では翻訳に関するTipsについて説明します。

適切なNSLocalizedStringのキーを設定する

 NSLocalizedString(_:comment:)のキーとして設定する値は非常に重要です。適切なキーを設定しなければ、あっという間に管理不能な状態になります。

 次のような、キーと値の組み合わせは良くありません。"latest"のような似通った表現が出てきた場合に、どこでどちらが使われているのか分からなくなる可能性があります。また、英語において、全て"new"と表現することができる箇所が、他の言語でも1つの用語で表現できるとは限りません。

"new" = "new";

 キーは常にコンテキストが分かるような値にしましょう。また、同じ単語であっても、コンテキストが異なれば、別のものとして定義するべきです。

"home.feed.new" = "new";

変数の順序に気をつける

 書式に変数を埋め込む形の翻訳対象の文字列があったとします。

"Copy %@’s %@" = "Copying %@’s %@";

 このように複数の変数がある場合、必ずしも他の言語でも上手く機能するとは限りません。例えば、これをそのままドイツ語にすると次のようになりますが、これは誤りです。ドイツ語では1番目の変数と2番目の変数の順序が逆である必要があります。

"Copy %@’s %@" = "%@ von %@ kopieren";

 この問題を回避するために、書式中の変数に対して、順序を指定することができます。

"Copy %@’s %@" = "%$2@ von %$1@ kopieren";

デバッグ

 本項ではデバッグに関するTipsを紹介します。

Double Length Pseudolanguage

 言語が増えれば増えるほど、文字列の長さを予測することは難しくなります。ある言語では1行で収まるような文言でも、他の言語では複数行となるといったようなことは頻繁に起こります。常に想定より長い文字列を表示できるようなレイアウトを組むべきです。

 「Double Length Pseudolanguage」というデバッグオプションを使用すれば、全てのローカライズされた文字列が2回繰り返される状態を再現できます。

f:id:yuseinishiyama:20160510150313p:plain

これによって、想定より長い文字列が入った場合にレイアウトが崩れるケースを検出できるかもしれません。次の例では、"bookmark"、"Create Recipe"などの翻訳対象の文字列が全て2回繰り返されており、ここから想定より長い文字列を表示すると一部のレイアウトが崩れることが検出できます。

f:id:yuseinishiyama:20160510150312p:plain

Right to Left Pseudolanguage

 「Right to Left Pseudolanguage」というデバッグオプションを使用すれば、擬似的なRTLの状態を再現することが可能です。

f:id:yuseinishiyama:20160510150505p:plain

 まだRTLの言語をサポートしていないアプリでも、RTLになった際にどのようなレイアウトになるか確認することができます。次の例では、言語は英語ですが、レイアウトはRTLになっています。

f:id:yuseinishiyama:20160510150419p:plain

NSShowNonLocalizedStrings

 NSShowNonLocalizedStringsを起動時のオプションに指定すれば、未翻訳の文字列のキーが大文字で表示されます。

f:id:yuseinishiyama:20160510150414p:plain

 例えば、次の例では、検索バー内のプレースホルダーが未翻訳のため、そのキーが大文字で表示されています。この結果、デバッグ時に翻訳忘れに気付きやすくなります。

f:id:yuseinishiyama:20160510150412p:plain

おわりに

 本記事では、CookpadアプリにおけるiOSアプリケーションの国際化と地域化について説明しました。

 言語毎にコンテンツやアカウントが異なるサービスというのは珍しく無いはずです。これをきっかけに、iOSアプリにおける言語の扱いに関する議論が盛んになれば、と思います。

 また、普遍的に役立つトピックもあれば、Cookpadアプリ固有のトピックもあったかと思います。最初に補足したように、いくつかの点で標準的な方法から逸脱した手段を取っています。我々のサービスでは、要求を満たすためにこうした手段を取りましたが、その代償として多少のメンテナンスコストの増加は避けられませんでした。もし、同様のことを行うのであれば、それが本当に必要かどうか、そしてそのコストがどれくらいかを検討するのにこの記事が参考になれば幸いです。

 このようにサービスの海外展開では、海外の文化、ユーザーのモバイル利用環境など様々な事柄を考慮しなければなりません。私たちは、こうした困難に積極的に立ち向かい、海外でクックパッドのサービスを展開することに協力してくれるモバイルエンジニアを積極募集中です!

弊社採用ページ(海外グループ iOS/Android アプリエンジニア)

*1:ローンチ後、Google Analyticsのデータからインドネシアからの多くのアクセスが、言語設定が英語のデバイスからであることが明らかになった。

*2:2016年5月10日時点の仕様。今後、変更される可能性がある。

日本語形態素解析の裏側を覗く!MeCab はどのように形態素解析しているか

$
0
0

こんにちは、買物情報事業部の荒引 (@a_bicky) です。 前回、「検索結果の疑問を解消するための検索の基礎」で単語単位でインデキシングする前提で説明しましたが、今回は文などを単語単位で分割するために使う技術である形態素解析について触れます。 形態素解析器には色々ありますが、中でもメジャーと思われる MeCabがどのように形態素解析しているかについて説明します。

MeCab の解析精度を上げるために辞書に単語を追加したことのある方もいると思いますが、動作原理を理解することで単語を追加する際に適切な生起コストを設定できるようになったり、学習の際に適切なパラメータを設定できるようになったりするはずです。

なお、MeCab は汎用テキスト変換ツールとしても使用できますが、簡単のため MeCab + IPA 辞書のデフォルト設定前提で説明します。

アジェンダ

  • 形態素解析とは
  • MeCab における最適な解析結果の推定
    • ラティスの構築と最適パスの選択
    • 未知語処理
    • 共通接頭辞検索 (common prefix search)
  • MeCab におけるコストの算出
    • CRF によるモデル化
    • 素性関数
    • モデルから生起コストと連接コストへの変換
  • 最後に

「MeCab における最適な解析結果の推定」では、形態素解析する際に MeCab が内部でどのようなことを行っているかについて説明します。「MeCab におけるコストの算出」は辞書を独自で追加したりモデルを再学習させたりする人向けの内容で、ある程度機械学習の知識を持っていることを前提として MeCab がどのようにコストを決定しているかについて説明します。

形態素解析とは

日本語の形態素解析では一般的に次の 2 つのことを行います。

  • 単語分割*1
  • 品詞付与

次の結果は「東京都に住む」を MeCab で形態素解析した結果です。入力文が適切に分割され、適切な品詞が割り当てられています。

% echo 東京都に住む | mecab
東京    名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー
都      名詞,接尾,地域,*,*,*,都,ト,ト
に      助詞,格助詞,一般,*,*,*,に,ニ,ニ
住む    動詞,自立,*,*,五段・マ行,基本形,住む,スム,スム
EOS

MeCab における最適な解析結果の推定

本節では MeCab を使って形態素解析する際に内部でどのようなことが行われているかを説明します。 単純化すると、最適な解析結果を求めるには次の 2 つのことを行います。

  1. ラティスを構築する
  2. ラティスから最適なパスを選択する

ラティスとは、考えられる全ての解を表現したデータ構造で、例えば次のようなデータです。

f:id:a_bicky:20160511155333p:plain

BOS は beginning of sentence で文頭、EOS は end of sentence で文末を意味しています。 各ラティスのノードには単語の生起コスト、エッジには品詞の連接コスト*2が割り当てられています。 最適なパスを選択する際には累積コストを最小にするパスを選択します。

f:id:a_bicky:20160511155409p:plain

累積コストを最小にするパスは動的計画法により効率的に求めることができます(ビタビアルゴリズム)。

ラティスの構築と最適パスの選択

ラティスを構築してから最適なパスを選択すると前述しましたが、実際にはラティスの構築と累積コストの計算は同時に行われ、ラティスの構築が完了した時点で最適パスが求まる形になっています。 具体的には次の手順によってラティスの構築と最適パスの選択を行います。

  1. n = 1 とする
  2. 入力文の n 文字目において分割候補になり得る単語を辞書から全て取得する (共通接頭辞検索)
    • カタカナ等同じ文字の種類の連続をひとまとめにした単語を未知語として候補に追加することもある
  3. 取得した全ての単語に関して、累積コストが最小になるエッジとその累積コストを求める
  4. n を +1 して入力文の末尾に到達するまで 2, 3 を繰り返す
  5. 末尾から先頭に向かって累積コストを最小にするパスをたどる(これが最適パス)

なお、単語の生起コストは sys.dic 等の辞書に、品詞の連接コストは matrix.bin (matrix.def) に保存されています。

上記の処理は次のスライドを見てもらうとイメージが付きやすいと思います。太いエッジは、右側のノード(単語)にとって累積コストが最小になるパスを意味しています。 左文脈 ID、右文脈 ID は特殊な使い方をしない限りは品詞 ID のようなものと理解しておけば大丈夫です。前件文脈 ID は連接する左側の単語の右文脈 ID、後件文脈 ID は右側の単語の左文脈 ID です。

実際に生起コストや連接コストを出してみるとスライドのとおりになっていることがわかります。-F オプションで出力形式を指定することで、デフォルトの内容に加えて生起コスト、連接コスト、累積コストも表示しています。

% # 表層形\t素性\t生起コスト,連接コスト,累積コスト
% echo 東京都に住む | mecab -F '%m\t%H\t%pw,%pC,%pc\n' -E 'EOS\t%pw,%pC,%pc\n'
東京    名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー  3003,-310,2693
都      名詞,接尾,地域,*,*,*,都,ト,ト   9428,-9617,2504
に      助詞,格助詞,一般,*,*,*,に,ニ,ニ 4304,-3573,3235
住む    動詞,自立,*,*,五段・マ行,基本形,住む,スム,スム  7048,-3547,6736
EOS     0,-409,6327

未知語処理

ラティスを構築する際、辞書に登録されている単語しか考慮しない場合、例えば次のように任意の数字を処理するには考えうる全ての値を辞書に登録しておかなければ正しく解析できません。

% echo 1234個 | mecab
1234    名詞,数,*,*,*,*,*
個      名詞,接尾,助数詞,*,*,*,個,コ,コ
EOS

辞書に存在しない単語(未知語)にも対応するため、MeCab では同じ文字の種類 (e.g. KATAKANA) でまとめて 1 つの単語とみなすようにしています。これによって、ラティスを構築する際に未知語のノードを追加し、未知語を含んだ解を選択することも可能になります。 未知語の生起コストに関しては文字の種類に応じて unk.dic (unk.def) に定義されています。

ラティスに未知語を追加するかどうかは char.defで制御されています。以下は char.def の該当部分です。

#  CHARACTER CATEGORY DEFINITION
#
#  CATEGORY_NAME INVOKE GROUP LENGTH
#
#   - CATEGORY_NAME: Name of category. you have to define DEFAULT class.
#   - INVOKE: 1/0:   always invoke unknown word processing, evan when the word can be found in the lexicon
#   - GROUP:  1/0:   make a new word by grouping the same chracter category
#   - LENGTH: n:     1 to n length new words are added
#
DEFAULT        0 1 0  # DEFAULT is a mandatory category!
SPACE          0 1 0  
KANJI          0 0 2
SYMBOL         1 1 0
NUMERIC        1 1 0
ALPHA          1 1 0
HIRAGANA       0 1 2 
KATAKANA       1 1 2
KANJINUMERIC   1 1 0
GREEK          1 1 0
CYRILLIC       1 1 0

2, 3, 4 列目が未知語処理に関する設定で、それぞれ次のような意味を持っています。

列数 名前 意味
2 INVOKE 1 であれば常に未知語を追加する。0 であれば、候補となる単語が見つからなかった場合にのみ未知語を追加する
3 GROUP 1 であれば同じ種類の文字を最大 max-grouping-size 文字まとめて 1 つの未知語として追加する
4 LENGTH 現在の位置から 1 〜 LENGTH 文字の部分文字列全てを未知語として追加する

GROUP と LENGTH の設定は互いに独立です。例えば、「ホゲホゲ」の 1 文字目の位置で単語を追加する場合、KATAKANA は GROUP が 1 なので「ホゲホゲ」という未知語を追加します。また、LENGTH が 2 なので、「ホ」と「ホゲ」という未知語も追加します。2 文字目の位置で未知語を追加する場合も同様に「ゲホゲ」、「ゲ」、「ゲホ」という未知語を追加します。

% echo ホゲホゲ | mecab
ホゲホゲ        名詞,固有名詞,組織,*,*,*,*
EOS

漢字は INVOKE が 0 で、単語が辞書にある場合は未知語を追加しないので、漢字で構成される固有名詞の解析は上手くいかないことが多いです。

% echo 荒引 | mecab
荒      名詞,固有名詞,人名,姓,*,*,荒,アラ,アラ
引      名詞,固有名詞,組織,*,*,*,*
EOS

共通接頭辞検索 (common prefix search)

ラティスを構築する上で、該当位置において候補となり得る全ての単語を辞書から取得する必要があります。

common_prefix_search("東京都に住む")  # => ["東", "東京"]
common_prefix_search("京都に住む")    # => ["京", "京都"]
common_prefix_search("都に住む")      # => ["都(名詞)", "都(接尾辞)"]

このような用途の検索は共通接頭辞検索 (common prefix search) と呼ばれています。共通接頭辞検索を高速に行うためには TRIE というデータ構造を利用するのが一般的です。 MeCab では TRIE の中でもダブル配列という実装を採用しています*3。ダブル配列については非常にわかりやすいエントリーがあるので興味のある方は次のエントリーを参照してみてください。

情報系修士にもわかるダブル配列 - アスペ日記

MeCab におけるコストの算出

形態素解析時には生起コストや連接コストは与えられている状態ですが、本節ではそれらのコストを事前にどのように決定しているかについて説明します。 コストの算出には大きく 2 つの手順があります。

  1. CRF によるモデル化
  2. モデルからコストの算出

MeCab には新しい単語を追加する際に自動で生起コストを推定する機能がありますが、モデルファイルが必要なのは、1 が終わった状態で 2 を行うためです。

余談ですが、あるドメインに頻出する単語を新しく辞書に追加する際、生起コストを自動推定しても実際よりも高くなることがあるはずです。 モデルはどの品詞が出現しやすいか、どの文字種が出現しやすいか、どの原形が出現しやすいか等の情報を保持していますが、新しく追加する単語の原形などの出現しやすさについての情報を持っていないからです。 個人的には、自動推定するよりも、同じぐらいの頻度で出現すると思われる単語の生起コストを新しく追加する単語に割り当てる方が確実だと思います。

それでは、モデル化からコストの算出まで説明していきます。

CRF によるモデル化

MeCab では生起コストと連接コストを決定する上で Conditional Random Fields (CRF) を使ってモデル化しています。CRF は系列ラベリングの一手法で、系列データが与えられると対応する系列ラベルを出力します。 MeCab の場合、入力の系列データは単語分割済みの文字列の配列、出力は品詞情報などを保持したオブジェクトの配列です。

f:id:a_bicky:20160511155013p:plain

CRF のモデルは次の式で表すことができます。φは素性関数で詳細は後述します。Z は確率の総和を 1 にするための正規化項です。

f:id:a_bicky:20160511155019p:plain

MeCab では、人手等で形態素解析した x, y の組み合わせに対し、正則化項も追加して次の目的関数を最小とするパラメータαを求め、αの値を基にコストを決定しています。

f:id:a_bicky:20160511155025p:plain

なお、MeCab ではモデルを再学習させることもできますが、その場合の目的関数は次のようになっています。

f:id:a_bicky:20160511155031p:plain

これは、元のモデルのパラメータから変化量が大きいと損失が大きくなるので、元のモデルをできる限り変化させないように最適化することを意味しています。

これらの目的関数は凸関数であり、MeCab では L-BFGS(準ニュートン法の一種)で解を求めています。

CRF については「言語処理のための機械学習入門」の 5 章「系列ラベリング」に非常にわかりやすくまとまっているので、詳細を知りたい方はそちらを参照してください。

素性関数

素性関数は、引数がその素性関数の条件を満たす場合に 1、そうでない場合に 0 を返す関数です。 MeCab で採用している CRF は次のグラフィカルモデルのように、t 番目の y は 1 つ前の y と現在の x にしか依存しないことを仮定しています。

f:id:a_bicky:20160511155037p:plain

よって、

f:id:a_bicky:20160511155043p:plain

と表すことができます。

素性関数の内容は feature.defを基に決定されます。それ故、feature.def の内容は素性テンプレートと呼ばれます。

ここで、例として素性テンプレートが次の場合を考えます。

UNIGRAM W0:%F[6]
BIGRAM B00:%L[0]/%R[0]

学習データは MeCab の出力と同じ形式のものを用意します。左の列が表層形で右の列が単語の素性です。 素性の内容は左から順に、品詞、品詞細分類1、品詞細分類2、品詞細分類3、活用型、活用形、原形、読み、発音になっています。

東京   名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー
都 名詞,接尾,地域,*,*,*,都,ト,ト
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
住む  動詞,自立,*,*,五段・マ行,基本形,住む,スム,スム
EOS

処理対象が「住む」の場合、素性テンプレートとテンプレートを基に生成される素性は次のとおりです。

素性テンプレート 素性
W0:%F[6] W0:住む
B00:%L[0]/%R[0] B00:助詞/動詞

%F[6] は処理対象の単語の7番目の素性、%L[0] は処理対象の単語の前の単語(左側の単語)の1番目の素性、%R[0] は処理対象の単語(右側の単語)の1番目の素性を意味しています。

素性テンプレートから生成された素性から、素性関数は次のように定義できます。

f:id:a_bicky:20160514005304p:plain

モデルから生起コストと連接コストへの変換

feature.def には unigram feature(現在の単語にしか依存しない素性)と bigram feature(現在と 1 つ前の単語に依存する素性)が存在します。 生起コストは unigram feature に属する素性関数を使って次のように算出されます。x は y の表層形、cost factor は dicrcに定義されている cost-factor です。

f:id:a_bicky:20160511154956p:plain

連接コストは bigram feature に属する素性関数を使って次のように算出されます。x は yjの表層形です。

f:id:a_bicky:20160511162416p:plain

mecab-ipadic の feature.def の bigram feature は品詞の情報しか使っていないため、MeCab の連接コストは品詞の情報しか考慮されていないことになります。よって、本エントリーでは連接コストのことを品詞の連接コストと呼んでいました。

最後に

以上、MeCab がどのように形態素解析しているかについて説明しました。 本エントリーを通じて、自然言語処理を応用した各種サービスの精度向上に少しでも貢献できれば幸いです。

*1:「形態素」とは意味の最小単位であり、本来は単語よりも小さな単位ですが、ほとんどの場合「単語」と解釈しても差し支えないでしょう

*2:本エントリーでは連接コストを品詞の連接コストと呼んでいますが、feature.def の定義によっては単語の連接コストにすることも可能です

*3:cf. http://chasen.org/~taku/software/darts/

Androidアプリのリソースを整理して開発効率を改善した話

$
0
0

技術部モバイル基盤グループの児山です。

モバイル基盤グループではモバイルアプリの開発だけでなく、開発環境の整備や開発効率の向上も重要な目的の一つとしています。 今回はその取組の中で、特にAndroidアプリの開発効率向上に関する取り組みを紹介したいと思います。

開発効率を下げる要因

経験上、どのようなアプリでも開発を続けていくうちに細かい技術的な負債がたまり、開発効率は下がっていくものです。 クックパッドアプリでは朝Lintの実施やDokumiによるレビューによってソースコードに技術的負債を溜め込まないよう心がけてきましたが、画像やレイアウトなどのリソースファイルは無法地帯になりつつありました。 以下にクックパッドアプリがかかえていた、リソース関連の問題をいくつか挙げてみます。

themeが整備されていない

Androidアプリでは、themeを定義し適用することで画面全体のUIを一括で設定することができます。 しかし、これまでのクックパッドアプリでは規則なく命名されたtheme定義が複数のファイルに散らばっており、どのthemeがどのようなUIを提供しているのか、代替リソースによって何が違うのか確認しづらい状態になっていました。 このため、開発効率の低下だけでなく、新しい画面を追加する際に既存のthemeとほぼ同じthemeを定義してしまうなど新たな負債を生む原因にもなっていました。

styleによるデザインの再利用ができていない

AndroidアプリではいくつかのUI属性をまとめてstyleとして定義し、そのstyleを複数のViewに適用することで、それぞれのViewの見た目を簡単に統一することができます。 しかし、styleの命名規則が定まっていなかったり文字サイズや文字色に決まったルールがないといった問題でstyleの再利用がうまくいかず、結局同じエンジニアが作成した2,3個のlayout間でしか再利用されていないということがよくありました。

文字の色、サイズ、書体などが整理されていない

アプリのデザインにおいて、全体の統一感を高めるためにも文字の色やサイズ、書体は表示箇所によって正しく使い分ける必要があります。 Androidアプリでは先述のstyleを利用することで使い分けが簡単にできるようになっていますが、クックパッドアプリではアプリ全体に関する標準的な文字サイズという定義がなかったため、各画面バラバラに定義されている状態でした。 その結果、文字サイズの直接指定箇所はレイアウトファイル内だけで400箇所以上、文字サイズのdimen定義は50個以上存在し、ほとんど再利用されないまま放置されていました。

エンジニアとデザイナの間に共通言語がない

styleの命名規則が統一されていなかったため、デザイナとエンジニアのやりとりはこの部分のテキストサイズが何sp、Boldで…と属性ごとの指定で行っていました。 標準となる文字サイズも決まっていなかったため1sp刻みで文字サイズを調整することもあり、「なぜこのサイズなのか」という疑問に明確な答えがだせずにもやもやしながら修正したこともあります。 クックパッドアプリでも、このエンジニア・デザイナ間のやりとりが一番開発効率を下げてしまっているようでした。

開発効率を上げるための工夫

ここまで書いてきたように、デザイン面の開発効率が下がって辛い感じになっていたクックパッドアプリですが、一番の問題は命名規則の混乱とそこから来る再利用性の低さにあることがわかりました。 これを解消するため、クックパッドアプリでは以下のような対応を段階的に進めていきました。

未使用のリソースを削除する

まず最初に、すでに使われなくなっているリソースを探して削除しました。 gradle plugin 1.4からはshrinkResourcesというオプションでビルド時に未使用リソースを削除することができるようになりましたが、開発中のファイル検索などでは普通に出てくるため残しておくと開発時に目にするリソース名が増えてしまい開発効率が下がります。

不要リソースの一覧は先ほどのshrinkResourcesの結果でも表示されますが、Android StudioでAnalyze -> Run Inspection by name -> Unused resourcesを実行することでも表示できます。

これらの方法ではdrawablelayoutのようなファイル単位ではなく、stringdimenstyleなどからも探すことができて非常に便利です。 実際にクックパッドアプリでは未使用リソースの整理だけで30個以上の未使用ファイルと1300行程度のリソース定義を削減することができました。

themeの定義

次に、themeの定義内容の整理を行いました。 先述の通りクックパッドアプリには元々いくつかのthemeが定義されていましたが、命名規則がばらばらであったり定義されているファイルがまちまちになっているという問題がありました。 また、タブレットやTVの対応を行ったりminSdkVersionを引き上げた際に代替リソースの整理をしておらず、すでに参照されなくなっている代替リソースが手付かずで残ったりもしていました。 これを修正するために、以下のような手順でリファクタリングを進めました。

  1. themeの定義をtheme.xmlに集約する(代替リソースの定義もtheme.xmlというファイルに集約する)
  2. themeの名前を規則性のある名前(AppTheme.*など)に揃える
  3. 直接指定するthemeと代替リソースを定義するthemeを分ける

1,2は通常のリファクタリングで行う作業と同じなので割愛し、3について説明します。

themeでは新しく追加された属性を使用したり、タブレットの場合は画面をダイアログのように表示したい、という場合にAPIレベルや画面サイズに応じて異なる記述が必要になることがあります。 これは代替リソースという仕組みを通じて切り替えることが可能ですが、代替リソースは差分のみうまく適用してくれるような仕組みなっていないため、同じthemeの代替リソースを定義すると本来必要な差分だけでなく共通部分もそれぞれ書く必要があります。

values/theme.xml

<style name="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar"><item name="android:textColor">@android:color/holo_red_dark</item></style>

values-*/theme.xml

<style name="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar"><!-- 本来必要な差分 -->
    ....

    <!-- この行の定義も必要 --><item name="android:textColor">@android:color/holo_red_dark</item></style>

これでは同じ内容の記述が分散してしまうので、一旦 代替リソースの実装はBase.AppThemeのような名前で行うようにします。 その上でAppThemeという代替リソースを持たないthemeを新しく定義し、Base.AppThemeparentに指定して継承させます。 最後にすべての代替リソースで共通な部分をAppThemeに集約することで、代替リソースで定義すべき差分のみをvalues-*配下のtheme.xmlに集約することができました。

values/theme.xml

<style name="AppTheme"parent="AppTheme"><item name="android:textColor">@android:color/holo_red_dark</item></style><style name="Base.AppTheme"parent="Theme.AppCompat.Light.DarkActionBar" />

values-*/theme.xml

<style name="Base.AppTheme"parent="Theme.AppCompat.Light.DarkActionBar"><!-- 本来必要な差分 -->
    ....
</style>

このようにthemeの定義を効率化することで、冗長な記述の削減しつつ参照すべき場所を限定することができ、開発効率を上げることができます。 (appcompatライブラリで提供されているTheme.AppCompat系のthemeも代替リソースをいくつも継承した構成になっており、効率化されていることがわかります)

styleの整理

styleについては記述箇所の整理と名前の整理を行いました。

styleはとても大雑把に分けるとthemeの属性に適用されるstyleと、layoutに適用するためのstyleに分類できます。 この2つが同じstyles.xmlに含まれていると、滅多に触らないthemeをいじるためにtheme.xmlstyles.xml両方を見る必要が出てきてしまい面倒です。 themeに適用するためのstyleはtheme.xmlに含めてしまうほうが良いでしょう。 稀にthemeとlayout両方で使われるstyleもありますが、その場合はthemeの属性に適用するstyleとしてtheme.xmlに含めたほうが良いと思います。

また、layoutに適用するstyleでは名前による継承をうまく利用することも大事です。 名前による継承とは、style名LargeTextというものを定義されている時、LargeText.Boldというstyleは自動的にLargeTextで定義された属性を引き継ぐことができるという機能になります。 例えば以下のように定義した場合、LargeText.Boldは自動的にテキストサイズ20spとなります。

<style name="LargeText"><item name="android:textSize">20sp</item></style><style name="LargeText.Bold"><item name="android:textStyle">bold</item></style>

残念ながらこれらの作業を簡単にやる方法はないので、定義済みのstyleに対して継承をうまく使いながら属性を減らす作業で地道に整理していくことになります。

その他のリソース整理

theme/style以外のリソースとして、color、dimenを整理しました。

colorの定義でも名前の付け方には注意する必要があり、画面名や機能名で付けてしまうと後々再利用しても良いかどうかわからなくなり多重定義されてしまう可能性が高くなります。 クックパッドアプリでは特によく使う色に関してはgreenorangeなどのわかりやすい名前で定義し、それがレシピ名のような特別な意味を保つ場合は<color name="recipe">@color/green</color>のように再定義しています。 この方法は一見複雑に見えますが、エンジニアからはレシピ名の色を探しやすく、デザイナーからは定義済みの色の種類をみつけやすくするための工夫になっています。

dimenに関しては、View要素のサイズ指定のどこまでをdimenとして定義するか悩ましく、クックパッドアプリでもまだまだ整理の途中となっています。 しかし、テキストサイズに関しては確実にdimenに集約しておくべきだと言えます。 クックパッドアプリではdimenの整理にあたって既存の文字サイズの定義を一旦捨てて、文字サイズをExtraLarge/Large/Default/Small/ExtraSmallの5段階に分けることにしました。 これまでのdimenを新しい5段階にあわせて分類する作業は大変でしたが、このルールを決めてからテキストサイズの指定時に何sp/何dpという混乱が起きなくなりました。

また、colorとdimenの整理にあわせてTextAppearance(TextAppearanceを継承したstyle)を定義することも非常に役立ちました。 TextAppearanceとは、Androidが提供している文字色や文字サイズ、フォントスタイルをまとめた小さなstyleのことで、文字表示に関する設定を一括で適用することができます。 非常に便利なので、クックパッドアプリでもこれを真似して独自のTextAppearanceを定義し、提供するようにしました。 クックパッドアプリでは文字に使える色は7色あり、そこに文字サイズが5段階と文字の太さが標準/太字の2種類、全部で70種類の組み合わせがあります。 この70種類の組み合わせについて、CookpadFont.Base.ExtraSmall.Whiteのようにそれぞれの属性がわかるような名前で定義し、実際に利用するもののみCookpadFont.Base.*を継承したCookpadFont.*、といった形で定義しています。 わざわざ継承させているのは理由があり、CookpadFont.Base.*階層で名前による継承を利用しやすくなるほか、同じCookpadFont.Base.*を利用する複数のCookpadFont.*をわかりやすくする目的があります。

text_appearance_base.xml

<style name="CookpadFont.Base"parent="TextAppearance.AppCompat" /><style name="CookpadFont.Base.ExtraSmall"><item name="android:textSize">@dimen/extraSmallTextSize</item></style><style name="CookpadFont.Base.ExtraSmall.White"><item name="android:textColor">@color/white</item></style>

text_appearance.xml

<style name="CookpadFont.CaptionWhite"parent="CookpadFont.Base.ExtraSmall.White" />

どう変わったか

上記のような様々な改修を加えた結果、クックパッドアプリではリソースのxml行数を2500行以上減らすことができました。 もともとそれだけ管理できずに肥大化していたということですが、改修後は新しいstyleやdimenを定義せずに既存のものを再利用するようになったおかげで新しい定義が増えることもほとんどなくなりました。 リソースの管理が適切にできるようになったと言えそうです。 今回の改修の中でもTextAppearanceの導入によって文字色・文字サイズの組み合わせに名前が与えられ、エンジニアやデザイナの共通言語として扱えるようになったのが特に大きな成果で、開発効率の低下を防ぐだけでなく向上させることができました。 リソースの整理は地道で根気のいる作業ですが、デザイン関連の開発効率の低下が気になっている方はぜひ検討してみては如何でしょうか。

TextAppearanceによるデザイン指示行われている様子 f:id:nein37:20160517114325p:plain

色や文字サイズにわかりやすい名前がついたのでTextAppearance自体の仕様変更も簡単です f:id:nein37:20160517114334p:plain

おわりに

本稿ではAndroidアプリの開発効率改善に関する最近の取り組みについてご紹介しました。 モバイル基盤グループでは引き続き最短距離でユーザーさんに価値を届けるための仕組みを考え、実施していきます。

弊社採用ページ(iOS/Android アプリエンジニア)

ImageMagickのピクセルキャッシュとリソース制限

$
0
0

こんにちは、成田(@mirakui)です。今日はみんな大好き ImageMagick チューニングのお話です。

2016/5/13 に公開された、いわゆる ImageTragickと呼ばれる脆弱性では、 policy.xmlというファイルを更新するという workaround が紹介されていたのは記憶に新しいと思います。

この policy.xmlは、今回の workaround のようにファイルタイプを制限するだけではなく、画像の縦横ピクセル数、利用するメモリやディスクのサイズなどを制限することができます。 Web サービスなどでユーザのアップロードした画像を ImageMagick で変換する場合、このようなリソース制限を適切に行うべきでしょう。

そこで今回は policy.xmlによるリソース制限方法を紹介します。

前提

特に明記しない限り、2016/05/14 現在の 6 系における最新開発版である ImageMagick 6.9.4-2 の仕様を基準にしています。

基本の書式

policy.xmlの基本の書式は以下のとおりです。

<policymap><policy domain="resource"name="temporary-path"value="/tmp"/><policy domain="resource"name="memory"value="256MiB"/><policy domain="resource"name="map"value="512MiB"/><policy domain="resource"name="width"value="8KP"/><policy domain="resource"name="height"value="8KP"/><policy domain="resource"name="area"value="128MB"/><policy domain="resource"name="disk"value="1GiB"/><policy domain="resource"name="file"value="768"/><policy domain="resource"name="thread"value="2"/><policy domain="resource"name="throttle"value="0"/><policy domain="resource"name="time"value="120"/><policy domain="system"name="precision"value="6"/><policy domain="cache"name="shared-secret"value="replace with your secret phrase"/><policy domain="coder"rights="none"pattern="EPHEMERAL" /><policy domain="coder"rights="none"pattern="HTTPS" /><policy domain="coder"rights="none"pattern="MVG" /><policy domain="coder"rights="none"pattern="MSL" /><policy domain="coder"rights="none"pattern="TEXT" /><policy domain="path"rights="none"pattern="@*" /></policymap>

ImageTragick 脆弱性の workaround では domain="coder"の設定だけを書いたと思いますが、それ以外にも上記のような設定項目があります。

なお、「基本の」と書きましたが、設定できる項目はこれで全てです。

以下のように、コマンドラインで現在の設定が確認できます。

$ identify -list resource
Resource limits:
  Width: 100MP
  Height: 100MP
  Area: 25.181GB
  Memory: 11.726GiB
  Map: 23.452GiB
  Disk: unlimited
  File: 768
  Thread: 12
  Throttle: 0
  Time: unlimited

本記事では、domain="resource"で指定できるリソース制限について紹介します。

ピクセルキャッシュが消費するリソース

policy.xmldomain=resourceで示されている「リソース」というのは、具体的にはピクセルキャッシュの記憶領域を指します。

The Pixel Cache - ImageMagick: Architecture

ピクセルキャッシュは、1ピクセルを表現する PixelPacket構造体を、画素数の分だけ並べた配列です。 ImageMagick は内部的にこのピクセルキャッシュで画像を表現しています。 画像を処理する場合には、このピクセルキャッシュの領域を確保するためにメモリやディスクといったリソースを消費することになります。

typedefstruct _PixelPacket
{
  Quantum
    blue,
    green,
    red,
    opacity;
} PixelPacket;

QuantumQ16でビルドした場合(デフォルト)は 2 バイト、Q8の場合は 1 バイトです。

つまり、Q16でビルドした ImageMagick において横 400 px、縦 300 px のピクセルキャッシュのサイズは以下のように求めることができます。

ピクセルキャッシュサイズ
  = width * height * sizeof(PixelPacket)
  = 400 * 300 * (4 * 2)
  = 960,000 [bytes]

リサイズ処理におけるピクセルキャッシュの利用例

ImageMagick の画像処理においてどのようなサイズのピクセルキャッシュが作られるかを説明します。

例として、以下のように convertコマンドで横 6,000、縦 4,000 ピクセルの JPEG 画像を 300x200 に縮小する場合のピクセルキャッシュ領域について考えます。

$ convert -debug All src.jpg -resize 300x200 dst.jpg

この場合、内部的には 3 サイズのピクセルキャッシュが作られます。

  1. 6000x4000 (183.1 MiB: 入力画像の展開用ピクセルキャッシュ)
  2. 6000x200 (9.155 MiB: リサイズ処理のためのピクセルキャッシュ)
  3. 300x200 (469 KiB: 出力画像用ピクセルキャッシュ)

メモリリソース上でのリサイズ処理では、この3つが同時にメモリ上に作られるため、合計 192.7 MiB のメモリリソースが消費されます。 つまり、6000x4000 ピクセルの画像をメモリ内で 300x200 にリサイズする場合には、メモリリミットを最低でも 192.7 MiB より大きく設定する必要があります。これがメモリリソースリミットです。

ちなみに、以下のブログ記事で紹介されているように JPEG の size ヒントを与えることによって、上記の例の場合は、リソース消費を 192.7 MiB を 4.463 MiB まで抑えることができました。

本当は速いImageMagick: サムネイル画像生成を10倍速くする方法 - 昼メシ物語

$ convert -debug All -define jpeg:size=300x200 src.jpg -resize 300x200 dst.jpg

policy.xml によるリソース制限

ピクセルキャッシュはメモリリソースを消費すると書きましたが、正確には、リソースは以下の3種類があります。

  • memory: メモリ
  • map: メモリマップドファイル
  • disk: ディスク

この記事の本題である policy.xmlでのリソース制限というのは、ピクセルキャッシュが消費するこれらのリソースを制限する、という意味です。

これらのリソースの挙動と制限について、policy.xmlに沿って説明します。

なお各リソースには、対応する環境変数が存在します。もし対応する環境変数が定義されている場合は、policy.xmlの値よりも環境変数の値が優先されます。 また、コマンドラインツールで -limit memory 256MiB -limit map 512MiBのようにリソースリミットを指定することもできます。この場合、環境変数よりもコマンドラインオプションの値が優先されます。

memory, map, disk

ピクセルキャッシュを作ることができるリソースには以下の3種類があり、それぞれ容量のリミットが設定されています。

リソース名 対応する環境変数 デフォルト値
memoryMAGICK_MEMORY_LIMITシステムのメモリサイズ [bytes]
mapMAGICK_MAP_LIMITシステムのメモリサイズ * 2 [bytes]
diskMAGICK_DISK_LIMIT unlimited [bytes]

ピクセルキャッシュは通常、メモリ上に作られます。

もしメモリのリソースリミット以上のサイズのピクセルキャッシュを作ろうとした場合、メモリマップドファイルが使われます。

さらにメモリマップドファイルのリソースが不足している場合は、ディスクに作られます。 ImageMagick のユーザなら、/tmp/magick-xxxxxというような名前の一時ファイルを見たことがあるかもしれません。これがディスクリソースに作られたピクセルキャッシュです。

以上の 3 リソースの制限値は、policy.xmlでは以下のように記述します。

<policy domain="resource"name="memory"value="256MiB"/><policy domain="resource"name="map"value="512MiB"/><policy domain="resource"name="disk"value="1GiB"/>

area

メモリ利用の制限には、memoryの他にも areaという値があります。

<policy domain="resource"name="area"value="128MB"/>
リソース名 対応する環境変数 デフォルト値
areaMAGICK_AREA_LIMITシステムのメモリサイズ * 2 [bytes]

areaリミットは、メモリに作ることを許す最大のピクセルキャッシュサイズです。

areamemoryとよく似ていますが、意味はやや異なります。

memoryリソースは複数回ピクセルキャッシュが作られると、都度消費されるものです。そしてピクセルキャッシュが不要になったときに解放されます。 例えば一連の処理で 100 KiB のピクセルキャッシュが 3 つ作成される場合、memoryリミットは 300 KiB より大きい必要があります。

それに対して areaは消費されるリソースではなく、メモリに作ることを許すピクセルキャッシュのサイズに対するリミットです。同様の例の場合は、areaリミットは 100 KiB より大きければ十分です。

ピクセルキャッシュ作成時における areamemorymapdiskの関係を擬似コードで表すと以下のようになります。

if作りたいピクセルキャッシュのサイズ < areaリミット &&
   作りたいピクセルキャッシュのサイズ < memoryリソース残量
  memoryリソース残量を消費してピクセルキャッシュを作成
elsif作りたいピクセルキャッシュのサイズ < mapリソース残量
  mapリソース残量を消費してピクセルキャッシュを作成
elsif作りたいピクセルキャッシュのサイズ < diskリソース残量
  diskリソース残量を消費してピクセルキャッシュを作成
elsif分散ピクセルキャッシュサーバ※が有効
  分散ピクセルキャッシュサーバ上でピクセルキャッシュを作成
elseエラー
end

この擬似コードからも分かるように、リソースのリミットが設定されていれば、リミットを超えた変換が走る前に失敗させることができ、リソースは消費されずに済みます。

※なお、分散ピクセルキャッシュサーバ(distribute-cache)については本題から外れるので詳しい説明を省きます。公式ドキュメントの "Distributed Pixel Cache"の項を御覧ください。

width, height

作成されるピクセルキャッシュの横、縦の長さに対して制限をかけることができます。

<policy domain="resource"name="width"value="8KP"/><policy domain="resource"name="height"value="8KP"/>
リソース名 対応する環境変数 デフォルト値
widthMAGICK_WIDTH_LIMIT 214.7 MP (Q16の場合。Q8なら429.5MP)
heightMAGICK_HEIGHT_LIMIT 214.7 MP (Q16の場合。Q8なら429.5MP)

なお、ImageMagick 6.9.4-1 までは、widthリミットで指定した値が heightリミットとしても使われてしまうというバグがあります。

この記事を書くためにソースコードを読んでいたらそのバグを発見したので、下記のプルリクエストを送ったところ、すぐにマージしていただくことができました。6.9.4-2 では直っていると思われます。

Fix typo s/width/height/ in resource.c (ImageMagick-6 branch) by mirakui · Pull Request #199 · ImageMagick/ImageMagick

その他

<policy domain="resource"name="temporary-path"value="/tmp"/><policy domain="resource"name="file"value="768"/><policy domain="resource"name="thread"value="2"/><policy domain="resource"name="throttle"value="0"/><policy domain="resource"name="time"value="120"/>
リソース名 対応する環境変数 デフォルト値
temporary-pathMAGICK_TEMPORARY_PATH, MAGICK_TMPDIR$TMPDIRの値( /tmpなど)
fileMAGICK_FILE_LIMITulimit -nの 3/4
threadMAGICK_THREAD_LIMIT OpenMPの最大スレッド数。OpenMP無効時は 1
throttleMAGICK_THROTTLE_LIMIT0 [microseconds]
timeMAGICK_TIME_LIMIT unlimited [seconds]

それぞれの値の意味は以下です。

  • temporary-path
    • /tmp/magick-xxxxxxのように、ピクセルキャッシュがファイルとして展開される際のディレクトリ
  • file
    • ピクセルキャッシュをディスク上で同時に展開できる最大個数。
  • thread
    • OpenMP で並列処理を行う最大スレッド数。一般的に、画像のリサイズ程度の処理では並列処理をしないほうが高速であることが多いです。並列処理を無効化する方法はいくつかありますが、この値を 1 にすることでも実現できます。
  • throttle
    • 並列処理を行う際、CPU 負荷を下げるための設定です。単位はマイクロ秒で、これが大きいほどピクセルキャッシュの走査処理を遅くし、負荷を下げることができるようです。ただし、私たちは並列処理を使ってないため詳細な性能は確認していません。
  • time
    • ピクセルキャッシュの走査処理におけるタイムアウト時間を指定します。単位は秒です。

リソース制限のチューニング例

ユーザからアップロードされた画像をオンラインでリサイズするというユースケースについて考えます。

この場合、もしかしたらユーザは巨大な画像をアップロードするかもしれません。 ファイルサイズが小さくても縦横のサイズが大きい画像というものを作ることは可能です。しかし ImageMagick でそれを愚直にピクセルキャッシュとして展開してしまうと、メモリやディスクが埋め尽くされる事になりかねません。

このようなユースケースの場合、私のおすすめは以下のとおりです。

  • memoryを、そのプロセスが使っていい最大の容量にする
    • areaの指定でもいいと思います。前述の通り似たような役割なので、areamemoryどちらかが書いてあれば事足りると思います。
    • アップロードされうる画像の最大の width, height が分かっている場合は事前に convert -debug Allオプションで表示されるデバッグログを見て、必要なリソース容量を見積もるのをおすすめします。
    • ピクセルキャッシュの容量を減らしたい場合は、JPEG 画像なら JPEG size hintを利用したり、ImageMagick を Q8 (--with-quantum-depth=8)でビルドしたりすると良いでしょう。
  • diskmap0Bにする
    • ユースケースによりますが、普通のスマホやデジカメで撮ったような写真であれば、オンメモリで処理できずにディスク I/O が走るような巨大リサイズは何らかの異常である可能性が高いと思います。こういった場合はそもそもディスクにピクセルキャッシュを書かせず、即エラーにしてしまう方が可用性にとって良いでしょう。
  • width, heightリミットはデフォルトのまま
    • 画像面積に対する制限がしたければ、 memoryもしくは areaだけで十分に役割が果たされるためです。そもそも前述の通り、widthのリミットが heightとしても使われてしまうというバグが 6.9.4-1 まであるので、それを理解したうえで使う必要があります。width, height の制限をかけたかったら、policy.xmlではなく、別途 identifyコマンドなどで調べたうえでアプリケーションから制限するのが現状では良さそうです。
  • thread1
    • 前述の通り、並列処理を無効にしたほうが画像リサイズは速いからです。
    • なお --disable-openmp--without-threadsオプションをつけてビルドされた ImageMagick の場合は、そもそも並列処理は無効になっているのでここを変更する必要はありません。

まとめると、下記のような設定があれば十分でしょう。

<policymap><policy domain="resource"name="memory"value="4GiB"/><!-- 容量は環境に合わせて調整 --><policy domain="resource"name="map"value="0B"/><policy domain="resource"name="disk"value="0B"/><policy domain="resource"name="thread"value="1"/><!-- 並列処理が有効なビルドの場合 --></policymap>

まとめ

本記事では ImageMagick におけるピクセルキャッシュの仕組みと、そのリソース制限について紹介しました。

今回紹介した内容の中には公式ドキュメントを読んだだけでは分からない仕様が含まれているので、皆様のチューニングのお役に立てれば幸いです。

Viewing all 726 articles
Browse latest View live