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

Prebid.js 導入による Header Bidding 改善の舞台裏

$
0
0

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

以前、"Header Bidding 導入によるネットワーク広告改善の開発事情"というタイトルで、

  • Header Bidding の仕組み
  • 弊社の広告配信のクライアント側の設計
  • Transparent Ad Marketplace(以下、TAM)導入の過程

についてご紹介しました。今回は、TAM に次いで Prebid.jsをあわせて導入した際の知見についてご紹介します。

What is Prebid.js?

Prebid.jsとは、OSS で開発されている Web 向け Header Bidding ライブラリです。 http://prebid.org/において開発されているサービスの1つで、他にはアプリ向けの Prebid Mobile、サーバーサイド向けの Prebid Serverなどが開発されています。

https://github.com/prebid/Prebid.js/

Prebid のサービス群を用いると、以下二種類いずれかの Header Bidding に対応できます。

  • Client-to-Server Header Bidding(以下、C2S)
    • Prebid.js 及び Prebid Server を用いる
  • Server-to-Server Header Bidding(以下、S2S)
    • Prebid Server を自前でホスティングするか、すでに提供されている第三者サーバーを利用する

C2S 及び S2S の Pros/Cons は以下の通りです。

TypeProsCons
C2S対応事業者数が多い/トータルでの実装コストが低いクライアントのネットワーク帯域をより消費する
S2Sサーバーサイドで制御できる/クライアント側からは Single Request対応事業者が C2S と比べて限られている/Cookie Sync の技術的課題

今回は、「対応事業者数が多い」こと、及び「実装コストの改修」を考慮して、C2S 方式を導入しました。

Glossary

本文で言及している用語のうち、ドメイン知識のため説明が必要と思われる用語の一覧です。

word description
事業者 Header Bidding で入札リクエストを送っている先の事業者のこと。ここでは基本的に SSP(DSP を接続する場合もある)のことを指す。
スロット 広告が実際に表示される枠のこと。
DFP DoubleClick For Publisher の略。新名称は Google Ad Manager。
APS Amazon Publisher Services の略。TAM などの一連の広告サービスを提供する総称。
TAM Transparent Ad Marketplace の略。APS のサービスの一つで、Header Bidding を提供する。

Development with Prebid.js

Prebid.js を導入する際に、基本的に 公式ドキュメント | Getting Started及び Publisher API Referenceを参考することになります。公式ドキュメントがかなり充実しているので、基本的なユースケースであれば、ほぼ嵌らずに実装できるでしょう。

以下は、Getting Startedに紹介されている、最小限の実装例です。ここで使用されている API のうち、主要なものについて紹介します。

なお、Google Publisher Tag(以下、GPT)との併合を前提としています。

<html><head><linkrel="icon"type="image/png"href="/favicon.png"><script async src="//www.googletagservices.com/tag/js/gpt.js"></script><script async src="//acdn.adnxs.com/prebid/not-for-prod/1/prebid.js"></script><script>var sizes = [[300, 250]];var PREBID_TIMEOUT = 1000;var FAILSAFE_TIMEOUT = 3000;var adUnits = [{                code: '/19968336/header-bid-tag-1',                mediaTypes: {                    banner: {                        sizes: sizes}},                bids: [{                    bidder: 'appnexus',                    params: {                        placementId: 13144370
}}]}];// ======== DO NOT EDIT BELOW THIS LINE =========== //var googletag = googletag || {};            googletag.cmd = googletag.cmd || [];            googletag.cmd.push(function(){                googletag.pubads().disableInitialLoad();});var pbjs = pbjs || {};            pbjs.que = pbjs.que || [];            pbjs.que.push(function(){                pbjs.addAdUnits(adUnits);                pbjs.requestBids({                    bidsBackHandler: initAdserver,                    timeout: PREBID_TIMEOUT});});function initAdserver(){if(pbjs.initAdserverSet)return;                pbjs.initAdserverSet = true;                googletag.cmd.push(function(){                    pbjs.setTargetingForGPTAsync && pbjs.setTargetingForGPTAsync();                    googletag.pubads().refresh();});}// in case PBJS doesn't load            setTimeout(function(){                initAdserver();}, FAILSAFE_TIMEOUT);            googletag.cmd.push(function(){                googletag.defineSlot('/19968336/header-bid-tag-1', sizes, 'div-1')
                   .addService(googletag.pubads());                googletag.pubads().enableSingleRequest();                googletag.enableServices();});</script></head><body><h2>Basic Prebid.js Example</h2><h5>Div-1</h5><divid='div-1'><scripttype='text/javascript'>                googletag.cmd.push(function(){                    googletag.display('div-1');});</script></div></body></html>

pbjs.addAdUnits

事業者ごとの設定項目を、スロットごとに追加します。 実質的には、pbjs.adUnitsフィールドに渡された引数を追加しておくだけです。

Source Code: - pbjs.addAdUnits()

この際、事業者ごとの設定項目を設定する必要がありますが、全て以下のドキュメントに記述されています。

入札者ごとの設定項目ドキュメント: http://prebid.org/dev-docs/bidders.html

ただし、以下の注意点があります。

  • 事業者ごとに、パラメータの型が String / Number / Object で差異がある
    • ex. placementIdが、文字列のこともあれば数字のこともある
  • ドキュメント上は任意(optional)だが、事業者から「必ず付与してください」と言われる場合がある
  • 一部ドキュメントが古い可能性があり、そのタイミングで事業者に最新のパラメーターを聞く必要がある

pbjs.requestBids

実際に Header Bidding 入札リクエストを行っている、要のメソッドです。

  • 設定された全事業者に対してリクエストを行う準備をする
  • auctionManager クラスを通して、auction を生成する
  • auction.callBids()で実際にリクエストを行い、入札結果レスポンスが返ってきたら、callback を実行する

Source Code: - pbjs.requestBids() - auctionManager.createAuction() - auction.callBids()

pbjs.setTargetingForGPTAsync

Header Bidding 入札結果を、GPT の Key/Value に設定します。したがって、入札が完了した後に呼び出す必要があります。

Source Code: - pbjs.setTargetingForGPTAsync() - targeting.setTargetingForGPT()

Prebid.js のソースコードを追っていくとわかりますが、入札結果の存在したスロットに対して、gpt.PubAdsService.setTargeting()を呼び出しています。

/**   * Sets targeting for DFP   * @param {Object.<string,Object.<string,string>>} targetingConfig   */
  targeting.setTargetingForGPT = function(targetingConfig, customSlotMatching) {window.googletag.pubads().getSlots().forEach(slot => {// ...
      slot.setTargeting(key, value);
    })
  };

Debugging Prebid.js

Prebid.js には、公式で数々のデバッグ方法やベストプラクティスが紹介されています。主に以下のドキュメントに詳しいです。

開発者用デバッグモードやChrome Extension などのツールも揃っており、入札フローが複雑な割には比較的デバッグがしやすい印象です。

主要なものについて紹介します。

Debug Log

pbjs.setConfig API には、Debugging option が提供されています。以下のように Option を渡すと、必要十分なログを出力してくれます。

pbjs.setConfig({ debug: true});

しかし、ブラウザリロード(pbjs の再読込)の度に設定がリセットされてしまうので、ブラウザの Console から打ち込む用途として利用するのでは不便です。

一方、弊社の広告配信サーバーの JavaScript SDK のビルドプロセスでは webpack を利用しており、ビルド環境(production/staging/development)を define-pluginを用いてソースコードに埋め込んでいます。

この仕組を利用し、ステージング及び開発環境では、デフォルトでデバッグログを有効にします。

// index.jsthis.pbjs.setConfig({
  debug: process.env.NODE_ENV === "development",
})
// webpack.config.js
plugins: [/**   * DefinePlugin create global constants while compiling.   *   * @doc https://webpack.js.org/plugins/define-plugin/   */new webpack.DefinePlugin({"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  }),
],

Snippets

Chrome で提供されている Snippetsという機能を利用し、本番環境でも手軽に入札リクエストや入札結果の中身をログに出力することができます。

これらのデータは Network タブから各事業者への入札リクエスト・レスポンスを覗くことでも確認できなくはないのですが、視認性が低いため Snippets を利用しています。

例えば、以下は Tips for Troubleshootingで紹介されている Snippets です。スロットごとに、全事業者に対する入札の結果、最終的に win した入札を表示してくれます。

var bids = pbjs.getHighestCpmBids();
var output = [];
for (var i = 0; i < bids.length; i++) {var b = bids[i];
    output.push({'adunit': b.adUnitCode, 'adId': b.adId, 'bidder': b.bidder,
        'time': b.timeToRespond, 'cpm': b.cpm
    });
}if (output.length) {if (console.table) {
        console.table(output);
    }else{for (var j = 0; j < output.length; j++) {
            console.log(output[j]);
        }}}else{
    console.warn('No prebid winners');
}

Chrome Extension

Prebid.js の公式ツールとして、Headerbid Expertという Chrome Extension が公開されています。

こちらのツールは、エンジニアだけでなくディレクターやプロジェクトマネージャーなども、気軽に自社の Header Bidding 入札結果を確認できるツールです。このツールを使うことで、事業者ごとの選別や、各社ごとのタイムアウト設定の見直し、インプレッション損失のリスクの洗い出しなどに利用できます。

f:id:itiskj:20190212120755j:plain
headerbid expert screenshhot

分析結果の見方については、Prebid.js Optimal Header Bidding Setupというドキュメントページに詳しく紹介されています。ある特定の事業者のタイムアウトに引っ張られて機会損失をしているパターン、何らかの設定ミスで DFP へのリクエストが遅れて機会損失をしているパターンなどが紹介されています。

Prebid.js Modules

Prebid.js は Module Architectureを導入しており、各事業者ごとのアダプターや通貨関連の共通処理をまとめたモジュールなどが提供されています。そして、ファイルサイズを可能な限り最小限に抑えるため、自社が必要なモジュールのみを Prebid.js Downloadページからダウンロードしたものを利用することが基本です。

Source Code: - src/modules/*.js

今回は、そのうちでも特に主要なモジュールについて紹介します。

Currency Module

Prebid.js を開発していると、入札結果の金額に関して、例えば以下のような要件が必ずと言っていいほど発生するはずです。

  • 事業者ごとに、net/gross が違うが、入札結果からオークションする前に net/gross の単位を統一したい
  • 入札金額の粒度をより細かくして、機会損失を最小限に抑えたい
  • JPY/USD などの通貨設定が事業者ごとに違うが、DFP にリクエストする前に通貨単位を統一する必要がある
  • 通貨単位を統一する場合、為替を考慮する必要がある

その場合は、公式で提供されている Currency Module を使うことになります。Currency Module を導入すると、pbjs.setConfig()に以下の設定項目を渡すことができるようになります。

Source Code: - currency.js

例えば、以下は設定例です。

this.pbjs.setConfig({/**     * set up custom CPM buckets to optimize the bidding requests.     */
    priceGranularity: "high",
    /**     * setting for the conversion of multiple bidder currencies into a single currency     * http://prebid.org/dev-docs/modules/currency.html#currency-config-options     */
    currency: {
        adServerCurrency: 'JPY',
        conversionRateFile: 'https://currency.prebid.org/latest.json',
        bidderCurrencyDefault: {
            bidderA: 'JPY',
            bidderB: 'USD',
        },
        defaultRates: {
            USD: {
                JPY: 110,
            }},
    },
});

conversionRateFileには、為替レートを変換する時に参考にする為替レートが格納されたファイルの URL を設定することができます。独自で更新したい場合は、こちらを自社の S3 Bucket などを見るようにしておいて、別途ファイルを更新するような仕組みを導入すればよいでしょう。デフォルトでは、jsDelivrと呼ばれる Open Source CDN に配置されてあるファイルを見に行くようになっています。

// https://github.com/prebid/Prebid.js/blob/master/modules/currency.js#L8const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$';

もちろん、毎回 Network を通じてファイルを取得しているわけではなく、Currency Module 内部でオンメモリに為替レートをキャッシュしています。

// https://github.com/prebid/Prebid.js/blob/c2734a73fc907dc6c97d7694e3740e19b8749d3c/modules/currency.js#L236-L240function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) {var conversionRate = null;
  var rates;
  let cacheKey = `${fromCurrency}->${toCurrency}`;
  if (cacheKey in conversionCache) {
    conversionRate = conversionCache[cacheKey];
    utils.logMessage('Using conversionCache value ' + conversionRate + ' for ' + cacheKey);
  }// ...

なお、https://currency.prebid.org/latest.jsonというファイルが、https://currency.prebid.orgにて提供されています。curl --verboseした結果が以下のとおりです。

curl --verbose http://currency.prebid.org/ | xmllint --format -
* TCP_NODELAY set
* Connected to currency.prebid.org (54.230.108.205) port 80 (#0)
> GET / HTTP/1.1
> Host: currency.prebid.org
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Connection: keep-alive
< Date: Fri, 08 Feb 2019 04:17:03 GMT
< x-amz-bucket-region: us-east-1
< Server: AmazonS3
< Age: 43
< X-Cache: Hit from cloudfront
< Via: 1.1 31de515e55a654c65e48898e37e29d09.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: XEpWTG_WXRO4w44X9eIrOV2r_sR-i9EyoZpUwhIRkzXwzqr71w1GyQ==
<
{ [881 bytes data]
100   869    0   869    0     0  27899      0 --:--:-- --:--:-- --:--:-- 28032
* Connection #0 to host currency.prebid.org left intact
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>currency.prebid.org</Name>
  <Prefix/>
  <Marker/>
  <MaxKeys>1000</MaxKeys>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>latest-test.json</Key>
    <LastModified>2018-10-15T21:38:13.000Z</LastModified>
    <ETag>"513fe5d930ec3c6c6450ffacda79fb09"</ETag>
    <Size>1325</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>latest.json</Key>
    <LastModified>2019-02-07T10:01:03.000Z</LastModified>
    <ETag>"6e751ac4e7ed227fa0eaf54bbd6c973d"</ETag>
    <Size>1331</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>test.json</Key>
    <LastModified>2018-12-05T11:00:47.000Z</LastModified>
    <ETag>"c4a01460ebce1441625d87ff2ea0af64"</ETag>
    <Size>1341</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

結果から、次のことがわかります。

  • Amazon S3 に格納されている
    • Server: AmazonS3
  • CloudFront で配信されている
    • X-Cache: Hit from cloudfront
  • latest.json / test.json / latest-test.jsonが提供されている

特に理由がないのであれば、こちらのファイルを使うことで概ね十分だと言えるでしょう。

また、net/gross の変換には、pbjs.bidderSettings | bidCpmAdjustmentを用います。

this.pbjs.bidderSettings = {
    bidderA: {
        bidCpmAdjustment : (bidCpm) => bidCpm * 0.85,
    },
    bidderB: {
        bidCpmAdjustment : (bidCpm) => bidCpm * 0.80,
    },
};

Integration with TAM

"Header Bidding 導入によるネットワーク広告改善の開発事情"でお伝えしたとおり、いくつかの事業者についてはすでに TAM 経由で Header Bidding 入札を行っていました。今回は、TAM と並行する形で Prebid.js の導入をする必要がありました。

以下に、全体のデータフローを示しました。parの部分で、TAM および Prebid.js 経由の Header Bidding 入札を並行して行い、両者から結果が返ってきたら DFP にリクエストします。

type description
ads 社内広告配信サーバ
display.js 広告表示用の JavaScript SDK
cookpad_ads-ruby display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
apstag TAM の提供する Header Bidding 用ライブラリ
googletag DFP の提供するアドネットワーク用ライブラリ
pbjs Prebid.js 用ライブラリ
SSP SSP 事業者(実際は複数事業者が存在している)

f:id:itiskj:20190212120829j:plain
Sequence Diagram for Prebid.js and TAM migration

DFP にリクエストをする前に、TAM と Prebid.js 両者の入札を完了させておきたかったので、Promise.allでリクエストを行い、待ち合わせる形で実装しました。以下は、本番で利用しているコードの抜粋です(エラーやロギングなど、本質ではない行を削除したもの)。

  requestHeaderBidding(slots) {const prebidPromise = this.requestPrebid(slots);
    const apsPromise = this.requestAPS(slots);

    return Promise.all([prebidPromise, apsPromise])
      .then(() => this.headerBiddingFinishCallback())
      .catch(err => Logger.error(err));
  }

  requestPrebid(slots) {returnnew Promise((resolve) => {
      pbjs.que.push(() => {
        pbjs.addAdUnits(this.getPrebidAdUnits);

        pbjs.requestBids({
          bidsBackHandler: (result) => {
            resolve({
              type: "prebid",
              result: result || [],
            });
          },
          timeout: this.prebid_timeout,
        });
      });
    });
  }

  requestAPS(slots) {returnnew Promise((resolve) => {
      apstag.fetchBids(thihs.apstagBidOption, (bids) => {
        resolve({
          type: "aps",
          bids: bids || [],
        });
      });
    });
  }

  headerBiddingFinishCallback() {
    googletag.cmd.push(() => {
      pbjs.setTargetingForGPTAsync();
      apstag.setDisplayBids();

      googletag.pubads().refresh();
    });
  }

Conclusion

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。特に、Prebid.js は、開発自体はドキュメントが丁寧な分嵌りどころは少ないものの、実際の導入フローにおける知見は、日本においてほとんど共有されていません。そこに問題意識を感じたため、この機会に Prebid.js の導入フローを紹介させていただきました。

広告領域は、技術的にチャレンジングな課題も多く、かつ事業の売上貢献に直結することが多い、非常にエキサイティングな領域です。ぜひ、興味を持っていただけたら、Twitterからご連絡ください。

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


レシピのタイトルから材料を予測する🚀

$
0
0

研究開発部のサウラブです。

本稿ではユーザがレシピの作成にかける労力を減らすために取り入れた、機械学習を利用した機能の一つについて 解説します。この機能を利用すると、ユーザがレシピのタイトルを入力することで、利用されるであろう材料が予測できます。

要約

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

App Storeで入手可能な最新のCookpadアプリ(v19.6.0.0)でこの機能を使用できます。

モデルはどうなっているか

1. Embed

f:id:bira:20190220104854p:plain

  • 学習(Training): Word EmbeddingとSentence Embeddingを学習してS3にアップロードします。(次のセクションで説明
  • 前処理(Preprocessing): 特殊文字を削除します。 多くのCookpadユーザーはテキストに特殊文字を使用しています。 例:"✧おいしい♡タンドリーチキン♡^-^✧"に特殊文字が含まれています: , ,^-^。特殊文字には材料に関する情報が含まれていないので、それらを削除します。特殊文字を削除するには、次のpython Functionを作成しました:

コードを表示する

import re
  defremove_special_characters(text):
      non_CJK_patterns = re.compile("[^"u"\U00003040-\U0000309F"# Hiraganau"\U000030A0-\U000030FF"# Katakanau"\U0000FF65-\U0000FF9F"# Half width Katakanau"\U0000FF10-\U0000FF19"# Full width digitsu"\U0000FF21-\U0000FF3A"# Full width Upper case  English Alphabetsu"\U0000FF41-\U0000FF5A"# Full width Lower case English Alphabetsu"\U00000030-\U00000039"# Half width digitsu"\U00000041-\U0000005A"# Half width  Upper case English Alphabetsu"\U00000061-\U0000007A"# Half width Lower case English Alphabetsu"\U00003190-\U0000319F"# Kanbunu"\U00004E00-\U00009FFF"# CJK unified ideographs. kanjis"]+",  flags=re.UNICODE)
      return non_CJK_patterns.sub(r"", text)

  • トークン化する(Tokenize): MeCabを使ってテキストをトークン化します。
  • Embedding: Word EmbeddingとSentence Embedding モデルを使用して、Cookpadデータベース内の各レシピのタイトルをベクトルに変換します。
  • 索引付け(Indexing): Faissを使用してベクトルにインデックスを付け(method = IndexFlatIP=Exact Search for Inner Product)、インデックスをS3にアップロードします。Faiss(Facebook AI Similarity Search)は、ベクトルの効率的な類似検索のためにFacebook AIによって開発されたライブラリです。 Faissは10億スケールのベクトルセットで最近傍検索をサポートします。

    2. Search&Suggest (API Server)

    f:id:bira:20190220104850p:plain

  • S3からWord EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをダウンロードします。
  • Word EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをメモリにロードします。
  • Embeddingモデルを使用して、入力されたタイトルをベクトルに変換します。
  • Faissを使用してk個の類似するレシピを検索します。
  • 類似するレシピの中で最も一般的な材料を提案します。

Embeddingsを学習する:

レシピのタイトルデータでWord Embeddingモデル(Fasttext)を学習します。

gensimでFasttextを使っていました。gensimはとても使いやすいです。

コードを表示する

from gensim.models import FastText
# recipe_titles : [.....,牛乳で簡単!本格まろやか坦々麺,...]# tokenize recipe titles using MeCab and then train fasttext model# recipe_title_list(tokenized) : [...,['牛乳','で','簡単','!','','本格','まろやか','坦々','麺'],....]
ft_model = FastText(size=100,min_count=5,window=5,iter=100, sg=1)
ft_model.build_vocab(recipe_title_list)
ft_model.train(recipe_title_list, total_examples=ft_model.corpus_count, epochs=ft_model.iter)

なぜFasttextを選んだのですか?

Fasttext(これは本質的にword2vecモデルの拡張です)は、各単語を文字n-gramで構成されているものとして考えます。 そのため、単語ベクトルは、これらの文字数n-gramの合計で構成されます。例:”中華丼”の単語ベクトルはn-gram”<中”、”中”、”<中華”、”華”、”中華”、”中華丼>”、”華丼>”のベクトルの合計です。Fasttextはサブワード情報で単語ベクトルを充実させます。それゆえ: - 稀な単語に対してもより良いWord Embeddingsを生成します。たとえ言葉が稀であっても、それらの文字n-gramはまだ他の単語中に出現しています。そのため、その Embedding は使用可能です。例:”中華風”は”中華丼”や”中華サラダ”のような一般的な単語と文字n-gramを共有することは稀であるため、Fasttextを使用して適切な単語のEmbeddingを学習できます。 - 語彙外の単語 - 学習用コーパスに単語が出現していなくても、文字のn-gram数から単語ベクトルを作成できます。

Sentence Embeddingモデルを学習します。

二つの Sentence Embedding モデルを試してみました:

  • Average of Word Embeddings:文は本質的に単語で構成されているので、単に単語ベクトルの合計または平均を取れば文のベクトルになると言えるかもしれません。 このアプローチは、Bag-of-words表現に似ています。これは単語の順序と文の意味を完全に無視します(この問題で順序は重要でしょうか?🤔)。

コードを表示する

import MeCab
  VECTOR_DIMENSION=200
  mecab_tokenizer_pos = MeCab.Tagger("-Ochasen")
  defsentence_embedding_avg(title, model=ft_model):
      relavant_words = [ws.split('\t') for ws in mecab_tokenizer_pos.parse(title).split('\n')[:-2]]
      relavant_words = [w[0] for w in relavant_words if w[3].split('-')[0] in ['名詞', '動詞', '形容詞']]
      sentence_embedding = np.zeros(VECTOR_DIMENSION)
      cnt = 0for word in relavant_words:
          if word in model.wv
              word_embedding = model.wv[word]
              sentence_embedding += word_embedding
              cnt += 1if cnt > 0:
          sentence_embedding /= cnt
      return sentence_embedding

  • トークン化する(Tokenize): MeCabを使用して文を形態素解析します。
  • フィルタ(filter) :名詞、形容詞、動詞だけを残して、他の単語を除外します。
  • 平均(Average): フィルタ処理した単語のWord Embeddingを取得し、それらを平均してタイトルベクトルを取得します。

  • Bi-LSTM Sentence Embeddings: Cookpadのレシピデータを使って教師あり学習によってSentence Embeddingを学習します。ラベルは2つのレシピ間のJaccard Similarityから導き出します。レシピを材料のセットと見なすと、2つのレシピ間のJaccard Similarityは次のように計算されます。 f:id:bira:20190220115601p:plain

    アイデアは、それらの間の高いJaccard Similarityを持つレシピのレシピタイトルベクトルをSentence Embeddingスペース内で互いに近くに配置することです。

    • データセットを作成します: 2つのレシピのタイトルと、これら2つのレシピの類似度を表すJaccardインデックスを含む各サンプル行を持つデータセットを作成します。{title_1, title_2, Jaccard_index}
    • 下のネットワークを学習します: f:id:bira:20190220104951p:plain上記のネットワークは2つの設定で学習することができます:
      • Regression: g(-) : sigmoid と y = Jaccard Index
      • Classification: g(-): dense+dense(softmax) と y = Jaccardインデックスから派生したクラスラベル 5クラスの分類設定で上記のネットワークを学習することによって学習されたF( - )は、最もよく機能するようです。ネットワークにとって、回帰問題よりも分類問題の方が解きやすい場合があります。

      Kerasでネットワークを実装する:

コードを表示する

from keras import backend as K
    from keras import optimizers
    from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np
    defcosine_distance(vects):
        x, y = vects
        x = K.l2_normalize(x, axis=-1)
        y = K.l2_normalize(y, axis=-1)
        return K.sum(x * y, axis=-1, keepdims=True)

    title_1 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    title_2 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_vec_sequence_1 = embedding_layer(title_1)  # Word embedding layer(fasttext)
    word_vec_sequence_2 = embedding_layer(title_2)  # Word embedding layer(fasttext)
    F = Bidirectional(LSTM(100))
    sentence_embedding_1 = F(word_vec_sequence_1)
    sentence_embedding_2 = F(word_vec_sequence_2)

    similarity = Lambda(cosine_distance)([sentence_embedding_1, sentence_embedding_2])
    similarity = Dense(5)(similarity)
    y_dash = Dense(5, activation='softmax')(similarity)
    model = Model(inputs=[title_1, title_2],  output=y_dash)

    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.fit([train_title_1, train_title_2], y)  # [train_title_1, train_title_2], y are respectively input titles and class label
    np.save('bilstm_weights.npy', F.get_weights())

  • 前のステップで学習したF(-)を文のEmbeddingとして使用します:

コードを表示する

from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np

    title = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_embedding = embedding_layer(title)
    F = Bidirectional(LSTM(100))
    sentence_embeddding = F(word_embedding)
    sentence_embedding_model = Model(input=title, output=sentence_embedding)

    sentence_embedding_model.layers[2].trainable = False
    sentence_embedding_model.layers[2].set_weights(np.load('bilstm_weights.npy'))
    defsentence_embedding_bilstm_5c(text):
        txt_to_seq = keras_tokenizer.texts_to_sequences([mecab_tokenizer.parse(text)])
        padded_sequence =  sequence.pad_sequences(txt_to_seq,maxlen=MAX_SEQUENCE_LENGTH)
        return K.get_value(sentence_embedding_model(K.cast(padded_sequence,float32)))[0]

結果

以下はサービスにおける利用率です。例えば、3 out of 5 suggested ingredients matches actual は 5 個 suggest したうち 3 個が利用された割合です。

3 out of 5 suggested ingredients matches actual(%) 2 out of 5 suggested ingredients matches actual(%)
Average of word embeddings 53% 80%
Bi-LSTM Sentence Embeddings 50% 76%

Average of word embeddings(これはBag-of-Wordsに似ています)はBi-LSTM Sentence Embeddingよりもこの問題に適しています。これは、レシピのタイトルは短いテキストであるために、単語順序の情報は材料を予測するのにはあまり役に立たないからだと思われます。

まとめ

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

いかがでしたでしょうか。 Cookpadでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。

春のインターンシップ自作キーボードコースのカリキュラムをちょっとだけ見せちゃいます!

$
0
0

Cookpad Spring 1day Internship 2019 自作キーボードコース、講師の KOBA789 です。

(インターンシップの正式名称長いな)

私事ですが、最近全自動洗濯乾燥機を買って生活が変わりました。

さて、募集開始時、にわかに界隈をお騒がせしておりました自作キーボードコースですが、気づけばもう当日まで1ヶ月を切ってしまいました。

講師の私は今必死に講義内容の準備をしているわけですが、今回は特別に、そのカリキュラムの一部をご紹介します。

そもそもどんな内容のコースなの?

募集ページには

このコースでは、キーボードの仕組みをハードウェア・ソフトウェアの両面から解き明かし、究極のキーボードをゼロから自作できる技術を身につけます。

Cookpad Spring 1day Internship 2019 | クックパッド 採用情報

と、大変抽象的な記述をしていました。

"ハードウェア・ソフトウェアの両面から解き明かし"って具体的に何よ、と疑問に思われたかと思います。

本コースでは、自作キーボードを設計するにあたり、「自作キーボード」という切り口ではたどり着きづらい知識・技術にフォーカスを当てて講義を進めていきます。

自作キーボードの設計方法については、他の方が書かれたよい資料がすでにいくつかあります。 キースイッチ・キーキャップの種類やキーのレイアウト、自作キーボードで一般的なマイコンやそのファームウェアなどの情報についてはそれらの資料で知ることができます。

実際、私もいくつか読ませていただき、大変参考になりました。

しかし、電子回路一般の知識やマイコンの仕様、USB のプロトコルなどについては、初学者にとってはどこから学んでいいものか皆目見当のつかない世界となっています。 このコースでは、自力だと入り口すらもわかりづらいような領域へ受講者のみなさんをご案内します。

はんだ付けをします

ほとんどの方の予想どおり、はんだ付けをします。

私の持論では、自作キーボードにとってはんだ付けは特に本質的な作業ではないと思っているのですが、とはいえ電子回路であるキーボードを組み立てるための手段としてのはんだ付けは便利であり、大切です。

部品を手にとってひとつひとつはんだ付けしていく中で、個々の部品をよく観察することにもなり、ハードウェアとの心の距離を近づけるための最初の一歩として適切でしょう。

また、なんであれ自ら苦労して組み立てたものに対する愛着というのは特別です。

キースイッチを分解します

キーボードの本質は何かと聞かれたら、それはやはり指先と電子回路の間にある部分、つまりキートップとキースイッチということになります。

メカニカルキーボードにおいて、我々の指と電子回路の世界をつなぐのはキースイッチしかありません。

そのキースイッチをよく観察することはキーボードを理解することであり、巨大な電子回路であるコンピュータと我々の関係性を見つめ直すことであります。

簡単な電子回路を学びます

キーボードは電子回路としてはとても簡単な部類ですが、とはいえ電子回路であることに違いありません。

いくら既存の事例を知っていたとしても、自分で設計をするときに最後に頼りにできるのは原理・原則だけです。

自分で回路を引くときに自信を持って引けるように、しっかり基礎を押さえます。

基板の設計を学びます

プリント基板を設計・製造してそこに部品を実装する、というのは自作キーボードのもっとも簡単な実装方法だと思います

なぜなら、多くのキースイッチがその実装方法を想定して設計されているからです。

本コースでもそれに倣い、KiCad を使って基板の設計をします。

USB HID について学びます

現代でキーボードを接続するといったらやはり USB を用いることになるでしょう。

USB の信号線はなぜ2本なのか、どんなプロトコルなのか、なぜ USB キーボードは6キーまでしか同時押しできないと言われているのかなど、様々な疑問にロジックアナライザと仕様書で答えていきます。

ファームウェアを自作します

本コースでは既存のファームウェアを利用しません。

講師がスクラッチから書き上げた USB プロトコルスタック込み1000行程度の小さなコードをベースに、自分だけのファームウェアを開発します。

講師が私(KOBA789)なので、実装言語は当然……わかりますよね?

これで全てではありません

以上だけでも既に1日で終わるのか怪しいボリュームになっていますが、ここに書いていない内容も計画しています。

最終的にどんなインターンシップになるのか(なったのか)は、きっと開催後のレポートでお伝えすることができると思います。

それではまた!

おまけ

先日、インターンシップ当日に使う部材を買い出しに秋葉原に行ってきました。

中央で後頭部が写っているのが私です。

f:id:koba789:20190220172616j:plain
みんな大好き秋月通商

試作用のキースイッチやキーキャップを買うため、遊舎工房にもお邪魔してきました。

f:id:koba789:20190220174958j:plain
遊舎工房の看板

そして、最後の1枚はチームメンバー4人で試作をしている様子です。

f:id:koba789:20190222123337j:plain
試作の様子
講義で当日使う基板が写っている部分はぼかしています。どんなキーボードを作るのかは当日のお楽しみ!

4人ともちゃんと動かすことができたため、きっと当日も大丈夫だと思います。

【開催レポ】Cookpad.apk#2を開催しました

$
0
0

こんにちは、Androidエンジニアしている吉田です。 2019/02/18(月)に弊オフィスで開催されたCookpad.apk#2の様子についてご紹介します。前回の様子については 下記のエントリを御覧ください。

techlife.cookpad.com

今回は弊社のエンジニア6名が発表致しました。本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

児山 千尋 「Androidアプリエンジニアの基礎知識」

f:id:kazy1991:20190225125527j:plain
Androidアプリエンジニアの基礎知識

はじめはモバイル基盤部のこやまカニ大好きからAndroidアプリエンジニアが知っていて欲しい基礎知識についての発表がありました。 Androidアプリエンジニアの知識とはFragmentのライフサイクルを正確に理解しているか?といった開発に直接関わる所だけに限らず、

  • Androidそのものに関する知識
  • Androidアプリの実装する上で必要な知識
  • ビジネス的な判断をサポートできる知識

の3つの観点からAndroidエンジニア全員に知っていてほしい知識が詰め込まれた内容が紹介されました。

篠原 弘光「Android Things Overview」

f:id:kazy1991:20190225134427j:plain
Android Things Overview

二人目はクックパッドマートshanonからAndroid Thingsについての発表がありました。 Android Thingsといえばつい最近衝撃的なニュースで話題になりましたが、詳細についてはご自身の目でスライドをお確かめください。

柴原 直也「cookpadTV のモジュール構成について」

f:id:kazy1991:20190225140349j:plain
cookpadTVのモジュール構成について

三人目はcookpadTVのFireTV/AndroidTVアプリを担当する梨原からモジュール構成について発表がありました。 モダンなAndroidアプリではマルチモジュールが当たり前になりつつありますが、その目的は様々です。cookpadTVでは様々なプラットフォーム対応しつつ高速に開発する手段としてマルチモジュールが採用されました。こちらも詳細は資料をご参照ください。

加藤 恭平「Espresso Driver を用いた Appium テストとその仕組み」

f:id:kazy1991:20190225142140j:plain
Espresso Driver を用いた Appium テストとその仕組み

四人目は技術部品質向上グループの加藤からテスト周りの技術であるEspresso DriverとAppiumの紹介がありました。 Appiumは人気の高いモバイルアプリケーション向けのE2Eテストフレームワークです。以前はUIAutomatorと組み合わせて使うことが一般的でしたが、内部状態に干渉できないため通信が終わるまでsleepを入れるなどのノウハウが必要でした。Espresso Driverが登場したことでContextにアクセスできるなどより高度な操作が可能になりました。

安部 建二「Dynamic feature moduleの基本」

f:id:kazy1991:20190225142537j:plain
Dynamic feature moduleの基本

五人目はKenji AbeからDynamic feature moduleについて発表がありました。 Dynamic feature moduleはアプリの機能の分割することでアプリ本体を小さく提供し、ユーザーが必要となったタイミングで各機能の実装を配信する仕組みです。本スライドでは実際にDynamic feature moduleをアプリに組み込んだ際のプラクティスや課題点が詳しく書かれています。

宇津 宏一「Kotlin Multiplatform Libraryのあれこれ」

最後は決算基盤グループのuzzuからKotlin Multiplatform Libraryについて発表がありました。 Kotlinにはマルチプラットフォーム構想がありJVMやAndroid環境以外でも動作する環境が提供されています。kotlinベースの複数環境を想定したプロジェクトを通称MPP(Multi Platform Project)と呼びます。クックパッドでは一部機能において「まずはAndroid向けのライブラリを開発できる事」をターゲットにMPPを利用し始めており、本スライドではMPP対応ライブラリを作るためのTipsや通常のAndroidプロジェクトからの利用にどう対処するのかなどコアな内容が紹介されています。

おわりに

ここ数年でAndroid開発は比較的安定した時期に入ったのかなと言う印象でしたが、今回のcookpad.apkではかなり尖った内容が多かったと思います。私達も試行錯誤を繰り返している最中で課題も多くありますが、スライドだけでは十分に伝わらなかった部分を議論したい、クックパッドの技術に興味を持ったという方がいらっしゃればぜひ弊社に遊びに来てください。
またクックパッドではAndroidエンジニアを随時募集しています。ご興味を持って頂けた方のご応募をお待ちしています。

マイクロサービス化を支える継続的切り替え術

$
0
0

こんにちはこんにちは。技術部のクックパッドサービス基盤グループのシム(@shia)です。グループ名が大きいですね。

クックパッドで運営しているサービスの中、一番古くから存在しているレシピサービス (cookpad.com) ——以下このサービスのコードベースを cookpad_all と呼びます——があります。 クックパッドサービス基盤グループはこのレシピサービスの運用及び改善という責務を持つグループとして今年の2月に発足しました。 わかりやすい業務の一つとしてはお台場プロジェクトが挙げられます。 お台場プロジェクトに関しては昨年12月の最後を飾った青木さんの クックパッド基幹システムのmicroservices化戦略 〜お台場プロジェクト1年半の軌跡〜という素晴らしい記事があるので紹介は省きます。

お台場プロジェクトの一つとして、僕は最近 cookpad_all からフィーチャーフォン向けのサービスである「モバれぴ」を分離するという作業をしています。このサービスはコードベースが大きいので、どうやって元のコードベースから新しい方に切り替えて行くのかが非常に大事です。この記事では現在進行形である、モバれぴの切り替え戦略に関して紹介してみようと思います。

始める前に

この記事でのサービス分離作業は巨大な一つのコードベースから、他に対する依存が少ない、もしくは関係のない機能・サービスなどをマイクロサービスとして分離することを意味します。ですので

  • コードベースが完全に分離される
  • 内部的には別のサービスとして提供する
  • 切り出し元と通信が必要であれば HTTP API や gRPC などの方法を用いる

を前提にしてお話していきます。

コードベースの切り替え戦略を考える

コードベース切り替えには大きく2つの戦略があります。 いわゆるビックバンデプロイと小分けにして継続的にデプロイしていく、というものですね。

モバれぴは完全に作り直すことにしたため、まずビックバンデプロイは無理だと判断しました。 仕様が同じとはいえ実装詳細が変わる以上、必ずなにかの問題が起こります。問題が起きると全体をロールバックするしかないので、全体をリリース & ロールバックが繰り返される状況になり作業は進まなくなるし、リリースのたびにユーザーに迷惑をかけることになります。 であれば取れる戦略としては後者のみですね。

昨年に取り組んだよく似ている事例として、iPhone/Android アプリ向け機能「料理きろく」のコード(API エンドポイント)を cookpad_all から1つのマイクロサービスとして切り出す(分離する)作業を行いました。 その時もビックバンデプロイは絶対避けたかったので、切り出し元であるサービスの手前でリバースプロキシとして存在している NGINX を利用し、新しいコードベースへ流すエンドポイントを指定できるようにし、これを増やしていくという方法を取りました。

NGINX の location で上流を切り替える戦略

f:id:riseshia:20190305074302p:plain
location を利用する切り替え戦略

この図では赤い矢印が切り替え完了したエンドポイントに対するリクエストで、青い矢印が切り替えが終わってないエンドポイントに対するリクエストです。 まずリクエストは ELB を通じてその後ろのリバースプロキシの役割を担当する NGINX へ流れます。そこでリクエストのパスを見て切り出し元にリクエストを流すか、切り出し先のサーバに流すかを決めるわけですね。 一つのエンドポイント(ここでは /v1/recipes)の実装を終えて、本番に投入したい状況になったとしましょう。このエンドポイントに対して以下のような NGINX 設定を書きます。

location ~ ^/v1/recipes {
  proxy_pass <切り出し先>;
}
location / {
  proxy_pass <切り出し元>;
}

このように location で新コードベースに流したいエンドポイントのパスを追加し、proxy_pass でリクエストを流せばいいわけです。

NGINX の location を利用した切り替え戦略の問題

すでにお気付きの方もいると思いますが、この方法はインフラ環境を頻繁にいじる必要があるという問題があります。

まず、本番環境のインフラをいじるだけで障害が発生するリスクが生じます。 そして特定のエンドポイントを切り替えるのにインフラ側とアプリ側のオペレーションが両方発生すること自体が不便です。 エンドポイントを一つ切り替えるだけなのに、新アプリをデプロイして、 NGINX をリロードして……エンドポイントが 20個ある場合、真面目にやっていくとしたらこれを 20回しないといけません。気が遠くなる面倒さです。 ということで手抜きをし始めると、1回に1個以上のエンドポイントを切り替えるようになるわけですが、これはビックバンデプロイに近づく結果になります。言い換えると、ロールバックの確率が高くなるとも言えるでしょう。 今考えるとエンドポイントの数が多くなかったとはいえ、よく頑張ったな〜という気持ちになります。

前回はインフラの作業コストとコピペに近い移植方式を考慮した結果、コントローラー単位で切り替えるようにしていました。 ですがモバれぴはビューの存在とすべてのリソースを API 経由で取得するというポリシーにしたため、作業コストが跳ね上がっていました。結果、いちいち移行していたらインフラ作業コストも跳ね上がるはずなので、いつまで経っても作業が進まなくなるのは明らかでした。

モバれぴでの切り替え戦略

前回、料理きろくの分離作業から得た教訓は

  • 切り替えでインフラオペレーションを使いたくない
  • ロールバックが簡単にできるようにしたい
  • 1アクション単位で切り替えたい

というものでした。つまり、切り替え作業コストを最小限に減らしたい。 これらの問題はすべてのリクエストを新コードベースに送り、新コードベースの方で、未実装の場合は切り出し元に問い合わせるよう、503(Service unavailable) ステータスコードを返すことで NGINX に要求することで解決できます。実際どんな感じにリクエストが処理されるのか図を見てみましょう。

f:id:riseshia:20190305074333p:plain
503 & proxy_next_stream を利用する切り替え戦略

赤い矢印が切り替え完了したエンドポイントに対するリクエストで、青い矢印が切り替えが終わってないエンドポイントに対するリクエストです。 図からもわかるように NGINX は 503 ステータスコードを受け取ったら切り出し元へリトライするだけです。どのリクエストを旧コードベースに流すか決めるのは切り出し先なので、こっちのコードを変更するだけで切り替え作業を完了します。

この方式は upstream の server の backup モードと proxy_next_stream 設定を利用すれば実装が可能です。具体的な実装方法の前にこれらの機能に関して説明します。

NGINX upstream のサーバ状態

まずは NGINX の upstreamに対して簡単に説明をしたいと思います。

NGINX の upstream は幾つかのサーバ群を一つのグループとして定義することができ、その内部でよしなにロードバランシングを行うことができます。さらに、グループとして定義した各サーバに対してどういうロードバランシング戦略をとるか、ラウンドロビンならどれくらいの重みでリクエストを流していくかなど、それなりに細かい設定を行うことができます。そしてここにはサーバの状態の扱いも含まれています。

upstream 内部で定義されたサーバの状態は2つ存在します。リクエストを受けられる状態(available)、リクエストを処理できない状態(unavailable)ですね。これはどういう条件で変化するのでしょうか? 答えは server ディレクティブで設定した fail_timeout(デフォルトは 10s) と max_fails(デフォルトは 1) にあります。 サーバに対して max_fails 回リクエストが失敗したら fail_timeout の間、そのサーバを unavailable とします。具体的な例を見てみましょう。

upstream backend {
    server backend1;
    server backend2;
}

このような設定があり、2つのサーバが仲良くリクエストを処理している状態から backend2 が何かしらに理由でリクエスト処理に失敗し、 unavailable 状態になりました。その後の 10秒の間は backend に送られるすべてのリクエストは backend1 が処理します。 ちなみに max_fails=0 にする場合、リクエスト処理に失敗したサーバは unavailable にはならず、常に available 扱いされるようになります。

ここまでの説明で質問が2つくらい浮かび上がると思います。

  • 失敗したリクエストはどう扱うのか?
  • そもそもここでの失敗は何を意味するのか?

失敗したリクエストは利用可能なサーバがあればそこへ再度流されます。これは NGINX の内部の挙動で、ユーザー側からは1リクエストとして認識される、ということを忘れないようにしましょう。もし利用可能なサーバが存在しなければ?バックアップのサーバが存在するのならそれを使います。バックアップは名前からも推測できると思いますが、普段は使われません。

upstream backend {
    server backend1;
    server backend2 backup;
}

このような設定がある場合、正常な状態では backend1 のみにリクエストを流します。もし backend1 が unavailable になるとその時初めて backend2 が利用されるようになります。 backend1 が available 状態に戻ったら backend2 はまた利用されなくなります。

そして NGINX はどのようなレスポンスを失敗として扱うのか。 これに関する回答は proxy_next_upstream (脚注: 使ってるモジュールによっては使える設定が違います。 http://nginx.org/en/docs/http/ngx_http_upstream_module.html#serverの max_fails オプションの説明に一覧が乗っているので参考してください)にあります。 この設定からはなにを持って失敗と定義するかを決められます。

デフォルトでは大雑把に言うとサーバにリクエストを流して NGINX がレスポンスを受け取りヘッダーを確認するまでの通信の中、なにか問題が起きれば失敗です。つまり、アプリが側の処理結果ではなく、通信がちゃんとできているかで判定している感じですね。 もちろん設定を変更することで 500、502、503 などのステータスコードを受け取った場合も失敗として扱うことが可能です。例えばサーバが 503 を返した場合を失敗扱いしたいのであれば

server {
  proxy_next_stream http_503;
}

のようにすればいいです。他のオプションや詳しい説明が必要であればドキュメントを参照してください。

503 と proxy_next_stream を組み合わせる

前述した方法を具体的に説明すると

  • 切り出し先で未実装なエンドポイントに対してすべて 503 を返す
  • 手前の NGINX では backup として切り出し元のサーバを指定し、 proxy_next_stream で 503 をリクエスト失敗と判定し、切り出し元にリトライする

ということをすればいいです。コードを見ていきましょう。

切り出し先では実装されてないエンドポイントなら形だけ作り 503 を返すようにします。 Rails であればコントローラに以下のような感じで書けます。

class RecipesController
  prepend_before_action :not_implemented, except: %i[edit]
  def create
    # ...
  end
  def show
    # ...
  end
  def edit
    # 移植完了したアクションでは普通のレスポンスを返せる
  end

  def not_implemented
    head :service_unavailable
  end
end

次は NGINX の設定ですね。

upstream backend {
    server new_backend max_fails=0;
    server old_backend backup;
}
server {
  proxy_next_stream http_503 non_idempotent;

  location / {
    proxy_pass backend;
  }
}

まずは max_fails を 0 にすることで、 new_backend が unavailable 扱いされることなくすべてのリクエストを処理するようにします。 そして proxy_next_stream で 503 ステータスコードを受け取ったら次のサーバにリトライするようにします。 old_backend を backup として設定するのは、リクエストに失敗したときのみ使われるようにするためです。 non_idempotent オプションを使うのは冪等ではないリクエスト(POST、PATCH など)の場合でもリトライを有効にするためです。 [脚注: non_idempotent を使っていいのか、の質問があると思いますが、この場合起りえる 503 は二種類があり、一つがこちらで意図して返す 503(未実装だよ〜)、新サービスの方から応答がないという本当の意味での 503 ですね。どちらの場合もリソースの処理をしないのでおかしな実装をしないかぎり問題ありません。]

ちなみになぜ 503 なのかといえば、

  • アプリケーション側から返す可能性が低い
  • proxy_next_stream でリトライすることができる
  • コードの意味がそれなりに自然に見える

という基準で選択しました。

503 と proxy_next_stream を利用した切り替え戦略の問題点

さて、最初のお気持ちをもう一回思い出してみます。

  • 切り替えにコストを使いたくない
    • head :service_unavailableを削除してデプロイすれば勝手に切り替わるので切り替えにかける追加コストはほぼゼロになりました
  • ロールバックを簡単にしたい
    • 通常のロールバックをするだけなので追加コストはゼロです
  • 1 アクション単位で切り替えたい
    • アクション単位で切り替えられるようになりました

すべての問題点を解決できていますね。ただ全てが便利になるのか?というと違います。例えば、以下のような不便さがありえます。

継続的な切り替えの場合は対象エンドポイント周りの切り替え作業中にはウェブ上のインターフェース(i.e. フォームで渡す引数の名前、ルーティングパスなど)を変えたい場合は慎重になる必要があると思いますが、この戦略の場合は特に切り替えの単位が細かいので、もっと気をつける必要があります。例えばフォームは旧サービスから返すのに、提出は新サービス、という状況もあり得るためですね。 同じ理由で、両サービスのルーティングテーブルは常に同期しておくのをおすすめします。

ちなみにモバれぴの場合、前述したようにアクティブな開発は行われていないため、この制約によるデメリットはありませんでした。

最後にレイテンシーに対する心配もあると思います。移植されてないエンドポイントの場合、 NGINX から新サービスへの 1RTT が無駄になるからですね。しかも Rails のミドルウェアを一周します。モバれぴでは、

  • リクエストの処理時間は ActiveRecord などの IO や、ビューレンダリングが支配的なのでこのように 503 を返すだけなら、そこまでは影響しないはず
  • ある程度のレスポンスの遅れは許容する
  • 移植作業が進むにつれ、その遅れは自然解消する

ということからこの方式で問題ないと判断しました。そして以下がその結果です。

f:id:riseshia:20190305074353p:plain
導入前後のレイテンシーの変化

これはモバれぴの一番前にある ELB の平均レイテンシーのグラフです。 1/17 の昼にてこの戦略のための NGINX 設定変更が行われますが、誤差の範囲に収まってるような印象を受けるので、大抵の場合は問題がないと思っても良いでしょう。

まとめ

この記事ではモバれぴを cookpad_all から分離する中、切り替え作業を継続的かつ小コストで進めるために選択した戦略、そしてそこに至った経験談、それに必要な背景知識などを説明しました。

もしこの話でお台場プロジェクトに興味が湧いたのであればぜひご連絡ください!

NPSアンケートを自動分類した話

$
0
0

研究開発部に2月18日から3月15日までアルバイトとして参加している岩手県立大学修士1年の橋本(@b_b_134)です。大学では画像認識について学びを深めています。

本稿では、アルバイトで行ったNPS®アンケート1の機械学習を用いた分類と、その結果を社内で活用してもらうための取り組みについて紹介します。

NPSアンケートについて

クックパッドではイベントやアンケートなどを通じてユーザーからの声を集計しています。その一環として、2年前からNPSアンケートを実施しています。

NPSアンケートはサービス(今回はクックパッド)を他者にオススメするかを調査するのが目的です。アンケートでは必須回答の「スコア」と任意回答の「コメント」を頂いており、スコアは0~10の値を取ります。

この「スコア」が9、10で回答してくださった方を推薦者、7、8で回答してくださった方を中立者、6以下で回答してくださった方を批判者と定義します(下図参照)。

f:id:kazuyuki-hashimoto:20190315174636p:plain

「NPS」の定義は推薦者の割合から批判者の割合を引いた値になります。

「コメント」は実に様々な意見が寄せられます。従業員全員が全て目を通せると良いのかもしれませんが、難しい話です。しかし、サービス改善に役立てられる貴重な声を無視することはできません。

現在は、社内の担当者が分析し社内で報告しています。社内ではこのアンケートをより一層活用したいという気運が高まっていました。私個人としてもユーザーがサービスについてどう思っているのかということに興味があったため、アルバイトで取り組むタスクに定めました。そこで、現在分析に携わっているスタッフとミーティングし、要件をまとめていきました。要件としては以下が挙がりました。

  • 頂いたコメントが何を話題にしているかを分類したい
  • ある話題に関するコメントのNPSの遷移をグラフなどで見たい

「推薦者はどこに魅力を感じているか」「批判者はどこに不満を感じているか」をより定量的な評価が可能になります。また、機能やサービスに関して早くフィードバックを得ることができます。

これまでも「頂いたコメントが何を話題にしているか」についてはルールベースおよび目視で分類してきました。しかし同時に、目視での作業量の多さなどから分類コストが大きいという問題がありました。そこで我々は機械学習によって分類コストを低減しました。

分析とデプロイ

では、NPSに関する課題を機械学習でどう解決したかを説明します。本プロジェクトは分析とデプロイ、二つのステージに分けられます。

ここで分析はコメントがどういった話題に関するものなのか予測するモデルを作成することを指します。 デプロイは作成したモデルをもとにアプリケーションを作成することを指します。ではまず分析でやったことを紹介します。

分析

今回は各NPSアンケートに含まれるテキストがどういった話題に関するものなのか機械学習を使って自動でラベル付けするモデルを作ります。過去に「検索」「レシピの量」など18種類に社内で人手でラベル付けしたデータがあったため、それを教師データとして教師あり学習を行いました。教師あり学習とは、あるデータに対して正解がわかっているとき、モデルがデータをもとに予測できるようにし、未知のデータが渡されたときに正しく予測出来るようにモデルを訓練することです。

構築したモデルを利用して、ラベルの付いていないコメントにラベルを付けます。今回はマルチラベル問題として取り組み、二値分類器を各話題ごとに構築して独立したラベルを付与しました。 このやり方は以前のtechlifeの記事ご意見分類業務と同様です。(下図参照)

f:id:kazuyuki-hashimoto:20190315174630p:plain

文章のベクトル化

まずは、アンケート文章を分かち書きし、ベクトル化します。 分かち書きは、以下のように単語ごとに文章を分割することです。

(元の文)
毎日の料理を楽しみにする。

(分かち書き後)
毎日 の 料理 を 楽しみ に する 。

導入の容易さから分析時点では分かち書きのフレームワークとしてJanomeを選択しました。今回はベクトル化ではTF-IDFを採用しましたが、将来的にfastTextやWord2Vecとの比較も考えています。

分類

機械学習でよく使われるscikit-learnの公式ドキュメントにはChoosing the right estimatorというページがあり、今回モデルの選定に利用しました。具体的には対象となるデータはラベルつきのデータがあり、かつ量が多くないためLinear SVCを選択しました。

ここでデータの内容を見てみましょう。

ラベル件数
機能 / 検索 251/1143
レシピ/量 341/1143
レシピ/難易度 152/1143
レシピ/味 177/1143
サービス全般/有料 124/1143
サービス全般/ユーザビリティ 45 /1143
サービス全般/役立ち度 463 /1143

上の表は全コメントに対して各ラベルが付与された比率が記述されています。なお、ラベルは1つのコメントにつき複数のラベルが付いていることがあります。サービス全般/役立ち度のように、正例が多い話題(463件)もあります。しかし、50件に満たないラベルもある不均衡なラベルもあります。

分析をし始めた当初、評価指標については、最初はAccuracyを見ていました。 しかし後日になって、社内のNPS担当者はラベル付けされたデータをさらに人手でチェックして、NPSデータを分析することが判明しました。 そのため、Accuracyを評価指標として使うより、関連する可能性のあるコメントを確実にラベル付けができるようにRecallを重視するほうがよいと考えました。 ちょうどscikit-learnにはclass_weightというパラメータがあり正例と負例の重みを調整できるパラメータが提供されています。 今回の実験ではこのパラメータ class_weightを利用してRecallの重みを大きくした場合についても合わせて検証しました。 単純なグリッドサーチを用いた場合とclass_weightを用いた場合の結果を以下の表にまとめます。

ラベル Recall
(GS)
Recall
(GS+CW)
f1
(GS)
f1
(GS+CW)
機能 / 検索 0.7500 0.8064 0.8413 0.8650
レシピ/量 0.7311 0.7999 0.8051 0.8241
レシピ/難易度 0.7804 0.8555 0.8707 0.8750
レシピ/味 0.5420 0.6600 0.6863 0.6000
サービス全般/有料 0.6712 0.6835 0.7967 0.7999
サービス全般/ユーザビリティ 0.4482 0.6071 0.6190 0.7555
サービス全般/役立ち度 0.8507 0.8505 0.8718 0.8868

※GS: GridSearch, CW: class_weight

上の表にはRecallとF1スコアが記述されており、それぞれ、GridSearchのみ、およびGridSearchとclass_weightを組み合わせた結果です。 上の表をみるとラベルによって、性能が出ているものと出ていないものがあるのがわかります。 十分なデータを確保できていないラベルに関しては性能が十分に出せていません。 しかし、自動分類の効果を確認してもらうため、まずは性能の高いラベルについてのみデータを分析者が見られるようにデプロイすることにしました。

デプロイ

前節で、プロジェクトの分析ステージが終わったので、デプロイに取り掛かります。 クックパッドでは研究開発部の各メンバーがデータの分析からモデルのデプロイまでの責任を持ちます(詳しくはこの記事を参照してください)。

今回のタスク、もともとの要件は以下の通りでした。

  • 頂いたコメントが何を話題にしているかを分類したい
  • ある話題に関するコメントのNPSの遷移をグラフなどで見たい

分析が終了しモデルが構築できたので、分類したデータをRedShiftに入れ、担当者が閲覧できる状態にすることを目標とします。

機械学習の結果をシステムに組み込む方法は逐次処理、バッチ処理に分けられます。 今回はリアルタイムに結果が分かる必要はないため、バッチとしてシステムに組み込むことにしました。 クックパッドではHakoというECS上に展開されるコンテナオーケストレーション環境があり、この上に先に構築した機械学習モデルをデプロイします。 システムの全体構成は以下のようになります。

f:id:kazuyuki-hashimoto:20190315174653j:plain
システム構成図

まずDockerコンテナ環境で実行できるCUIアプリケーションにします。 今回の分析ステージではJupyter Labを使いながら分析を進めたので、関数やクラスなどを抽出する作業が発生します。 このときデータ読み出し時のカラム名や前処理後のベクトルの形などに関するテストを順次追加しました。

次にCI上でテストを動作させる設定を追加しました。このときローカルファイルに依存したテストがFailしていたので、順次ファイル依存の問題を解決しました。 また、前処理の速度が気になる問題がありました。そのため分かち書きの処理をJanomeからMeCabに変更する修正も加えました。

次にHako上でアプリケーションとして動かせるようにします。クックパッドでは、バッチを実行する環境はスポットインスタンスを利用する環境と通常のインスタンスを利用する環境があります。 今回はHakoからS3へCSVの転送が途中で止まってもきちんと再実行できる設計にした上で、スポットインスタンスを利用する環境を選びました。

以上の取り組みから、毎月のデータをロード、NPSでの話題を分析し、RedShiftに保存するバッチフローが完成しました。

ここまでの機能を追加したあと、分析担当者に機能の共有をしました。ぜひこの内容を常に見られるようにしてほしいというフィードバックをもらいました。 そこで、より多くのスタッフにNPSのデータに興味を持ってもらいたいと考え、作成したグラフをSlackのボットで月次で投稿するようにしました。

ふりかえり

機械学習をプロダクションに組み込む作業は初めてでした。アルバイトを通じて機械学習の知識だけではなく、システム設計について考える点がたくさんあることがわかりました。

私は修士の学生として在籍している研究室では画像処理に取り組んでいます。 今回、はじめて自然言語処理のタスクを扱いましたが、思っていた以上に楽しかったです。 画像処理では慣れていることもあり、違和感なくベクトル化できますが、言語は一筋縄でいかないイメージがありました。 今回、自然言語処理タスクに取り組んだことで、この分野でも様々なツールが提供されかなり気軽に始められることが分かったのは収穫です。

今回のタスク(NPS)には自然言語の不均衡データという特徴がありました。 画像であれば、少ないデータに対して回転やクリッピングなどのデータオーギュメントを気軽に適用できます。 離散的な値を特徴とする自然言語では大きく意味が変わりかねず、類語を用いた言い換えは容易ではなく、目視で確認する作業が必要です。 このあたりについて、今後調べて見たいと考えています。


  1. Net Promoter®およびNPS®は、ベイン・アンド・カンパニー、フレッド・ライクヘルド、サトメトリックス・システムズの登録商標です。正味推奨者比率などと訳されます。

【RubyKaigi 2019 参加者に捧ぐ】福岡で起業した男が本気で書いた福岡グルメまとめ

$
0
0

f:id:kazzwatabe:20190319180852j:plain

CEO室で新規事業立ち上げをやりつつ、昨年子会社になりましたウミーベ株式会社の代表取締役をやっているカズワタベ(@kazzwatabe)です。

さて、来月には待ちに待った RubyKaigi 2019が開催されるんですが、クックパッドもRuby Committers' Sponsorとして関わっていたり、たくさんのエンジニアが現地参加するようです。

そんな RubyKaigi 2019 、今年の会場はウミーベが拠点とする福岡! そして福岡と言えば飯が美味い。これは他県から訪れる社内外のみなさんにグルメ情報を提供せねばという思いから、非エンジニアにも関わらず開発者ブログに登場する運びとなりました。

RubyKaigi 2019 に限らず、福岡を訪れる機会があったらぜひ行って欲しいお店ばかりなのでよかったら参考になさってください。

目次

海鮮

福岡では国内有数の漁場である玄界灘の海の幸が楽しめます。鮮度の高い刺し身を、甘み、旨味の豊富な九州醤油で食べるのがおすすめ。また、九州では関東では馴染みの薄い生のサバをよく食べます。特に胡麻だれに和えた「胡麻サバ」は絶品なのでぜひ食べてください。

サバ(きはる、独酌しずく)

tabelog.com

福岡の中でもサバがダントツで美味しいのがこちらの「きはる」。長崎県五島列島のサバを刺し身、炙り、胡麻サバにしていただくことができます。その他に、対馬の穴子の刺し身、天ぷら、焼きサバチャーハンなどがおすすめです。

tabelog.com

きはるが満席の場合は、同じサバが食べれる系列の「独酌しずく」へ。こちらの方が予約が取りやすい印象があります。

イカ(河太郎、表邸)

tabelog.com

佐賀県の呼子の名物である「イカの活造り」を食べれるお店が福岡にもあります。中でも有名店が「河太郎」。透き通ったイカの刺身はもちろん、後づくりとして出てくるエンペラやゲソの天ぷらは絶品です。

tabelog.com

他にイカの活造りが食べれるお店としては「表邸」が挙げられます。こちらは後づくりの天ぷらの衣にイカ墨を混ぜ込むのが特徴的。イカ以外も絶品ですよ。ほぼ全室個室なので、落ち着いた雰囲気で食事を楽しみたい場合におすすめです。

海鮮全般(ふじけん、兼平鮮魚店)

tabelog.com

tabelog.com

海鮮全般を楽しもうと思ったら「ふじけん」「兼平鮮魚店」がおすすめです。どちらも魚屋さんが営むお店で、その日仕入れた生きのいい魚を食べることができます。個人的には以前「ふじけん」で食べたサワラが奇跡的な美味しさだったのでまた食べたいです。

ランチにおすすめ(小野の離れ、梅山鉄平食堂、よし田)

ランチでも海鮮を楽しめるお店がたくさんあります。僕がよく行くのはこちらのお店です。

tabelog.com

僕が「福岡最強ランチ」と呼んでいるのが、こちらの「小野の離れ」。とりあえずヤバいんですが、どうヤバいのかは「小野の離れ ランチ」でググれば分かります。ランチも予約制で、12時スタート、13時半スタートの2回転のみです。予約取れたらラッキーなのでいますぐ電話かけましょう。ちなみに夜は夜でいい店です。

tabelog.com

もう少し気軽に海鮮ランチを楽しみたい方には「梅山鉄平食堂」がおすすめです。その日仕入れたたくさんの種類の魚を、塩焼きな煮付け、唐揚げなどにした定食を食べることができます。

tabelog.com

夜は割烹ですが、ランチはリーズナブルに「鯛茶(鯛茶漬け)」が楽しめるのが「割烹よし田」です。ぷりっぷりの真鯛と胡麻だれ、出汁の相性は格別。こちらも予約可能です。

もつ鍋(やま中、田しゅう、楽天地)

福岡の名物のひとつが「もつ鍋」。ぷりっぷりのもつに旨味たっぷりのスープ。ちゃんぽん麺や雑炊で〆まで楽しむことができます。がっつり食べたい方におすすめです!

tabelog.com

「やま中」は地元の人が観光客をよく案内する、高級もつ鍋屋の代名詞です。すでに行ったことがある方もいそうですね。

tabelog.com

やま中に負けず劣らず、より安く味わえるのが福岡で一番好きなもつ鍋屋が「田しゅう」。清潔感のある店内で、女性のみのグループもよく見られるのが特徴です。みそ味からのチーズリゾットで〆るのがおすすめです。

tabelog.com

最後に安く済ませたいときに使えるのが「楽天地」。食べ放題・飲み放題でもリーズナブル。ぷりぷりというよりは、コリコリしたもつを使った、B級グルメ感のあるもつ鍋の定番です。こっちはこっちで美味しいんですよね。

水炊き(とり田、とりぶどう HANARE)

もつ鍋と対を成す福岡名物が「水炊き」。鶏を長時間煮込んだコクのあるスープは絶品です。

tabelog.com

数ある水炊き屋の中でも大好きなのが「とり田」。2店舗ありますが、博多本店の方が広いので予約が取りやすいです。ここのスープは水筒に入れて持ち帰りたくなること間違いなしです。

tabelog.com

以前はとり田ばかり行ってたんですが、最近は「とりぶどう HANARE」もよく使います。焼き鳥の有名店「とりぶどう」の系列です。こちらはハツや砂肝など、一般的には水炊きに入れない部位が出てくるのが特徴。さらに本店の名物「幻の白レバー」も食べることができるんですが、その臭みのなさには驚きです。

焼き鳥(かわ屋、とりかわ粋恭)

tabelog.com

tabelog.com

福岡は実は人口10万人あたりの焼き鳥店店舗数が日本一で、美味しいお店がたくさんあります。中でも有名なのは「とり皮」が名物の「かわ屋」「とりかわ粋恭」の2店です。

少しずつ脂を落としながら、6日かけて焼いたカリカリのとり皮は絶品。人数×5〜10本くらいオーダーするのが福岡スタイル。こちらは会計も1人2〜3000円とリーズナブルです。

ラーメン(ShinShin、海鳴、おいげん、梟)

福岡でおすすめのラーメン屋の話をし始めると宗教戦争が始まるんですが、独断と偏見で選びます。

tabelog.com

週末は行列ができる「ShinShin」はクセの強くない豚骨ラーメンが名物。深夜まで営業しているので〆にも最高です。高菜トッピングがおすすめ。

tabelog.com

ちょっと変化球な豚骨ラーメンが食べたい方におすすめなのが、「ラーメン海鳴(うなり)」。こちらでは魚介とんこつや、豚骨をベースにイタリア風にしたジェノバ味などが楽しめます。魚介とんこつのスープは最後の一滴まで飲んでしまう魔力があります。

tabelog.com

天神から家に帰る途中にあったので、酔って帰る途中によく吸い込まれてたのが「ラーメンおいげん」。ShinShinに比べるとワイルドな豚骨スープが特徴です。炙ったチャーシューの香ばしさも相まって食が進みます。飲みの〆には最高です。

tabelog.com

こちらも〆によく使ってました。担々麺の名店「梟」。朝の5時までやってるのがありがたいです。辛さよりは旨味を感じるタイプの担々麺です。サイドメニューの串ホルも美味しいのでぜひ。

うどん(牧のうどん、釜喜利うどん、弥太郎うどん)

福岡の名物で、県外の人が真っ先に挙げるのはラーメンです。しかし住んでみるとそれ以上に食べる機会が多いのはうどんだということに気づきます。

うどんはコシがある讃岐うどんとは対照的で、柔らかい麺が特徴的。薄口しょうゆと出汁の透き通ったつゆでとても優しい味がします。具材はごぼ天(ごぼうの天ぷら)、丸天(さつま揚げみたいなやつ)が主流です。だいたいのうどん屋にはサイドメニューにかしわ飯(鶏の炊き込みご飯)があるんですが、こちらも美味しいのでぜひご一緒に。

tabelog.com

福岡らしい柔らかい太麺を楽しめるのが「牧のうどん」。福岡のソウルフードです。少し前まで中心部にはなかったんですが、博多駅に店舗ができて行きやすくなりました。麺がスープを吸ってしまうので、追加のスープがやかんが出てきます。

tabelog.com

個人的に一番好きなうどん屋が「釜喜利うどん」。薬院の有名なうどん居酒屋「二◯加屋長介」の姉妹店です(こちらもおすすめ)。スープも麺も具もすべてが美味しくて、今までの人生で食べた中でも一番のうどん屋です。意外と知られてないのですが、肉うどんにの具をご飯に載せた「和牛丼」が絶品なので、一度うどんを食べた人はぜひお試しください。

tabelog.com

福岡で飲み歩いてたら、みんな一度はお世話になっているであろうお店が「弥太郎うどん」。メイン通りである国体道路沿い、24時間営業という利便性は圧倒的です。〆にどうぞ。

その他(天ぷらのひらお、五穀、ヌワラエリヤ、たんか)

tabelog.com

名物ではないけど有名なのが「天ぷらのひらお」。あの味の天ぷら定食が700円ほどで食べれるのは奇跡です。東京だったら1500円でも安いくらいの味と量。テーブルに置いてある、食べ放題のイカの塩辛が隠れた人気メニューです。これが美味しすぎて天ぷら出てくる前に白飯がなくらないように注意が必要です。

tabelog.com

最近ドラマでも話題になった博多の明太子。その明太子をふんだんに使ったオムライスが食べられるのが「五穀」です。ふわふわのオムレツと、生明太子を和えたご飯の相性は抜群。行列必至なので余裕をもって行きましょう。

tabelog.com

住んで初めて知ったんですが、実は福岡はカレー文化が豊かです。中でも足繁く通っていたのが、スリランカカレーの名店「ヌワラエリヤ」。特にライスではなくビーフンを使った「ヌードルカリー」は絶品です。カレー好きの方はぜひ。

tabelog.com

最後になりますが肉系で一番好きなのが「たんか」です。牛タン、牛さがりの串焼きが出てくるんですが、その柔らかさと旨味にびっくり! これは言葉では説明しきれないのでぜひ行ってください。肉ならここです。

まとめ

他にも美味しいお店が無限にあるんですが、パッと浮かんだものを羅列しただけでこの量になってしまいました。福岡は罪深い土地です。RubyKaigi 2019にご参加の方はぜひ食も楽しんでください!

質問などありましたらTwitterで(@kazzwatabe)までお気軽にどうぞ。

福岡で働きたいエンジニア募集しています

また、そんな住環境最強都市福岡で働きたいエンジニアの方を、クックパッドの子会社であるウミーベで募集しております。ご興味お持ちいただけたら、ぜひウェブサイトからご連絡ください!

umee.be

入力時間11%減!書きやすいエディタのUIデザイン

$
0
0

こんにちは、投稿開発部の佐野大河(@sn_taiga)です。 先日、クックパッドのiOSアプリでレシピのエディタ画面をリニューアルしました。今日はそのUIデザインの設計についてお話します。

方針は「簡素化」

エディタ画面は、レシピを考えて記録・投稿する人にとって重要な機能の一つです。レシピには材料や作り方、料理写真、タイトル、紹介文などさまざまな項目があり、頭の中にある料理をこれらの形に落とし込んでいくのはなかなか大変な作業でもあります。なので、レシピを書く際の手間を減らし、ユーザーがストレスなくレシピを書けることを目的に「簡素化」という方針を定め、改善に取り組みました。具体的に行ったことは大きく以下の二つです。

1.入力や編集のステップを少なくする

以前のエディタ新しいエディタ
f:id:sn_taiga:20190329095727g:plain:w300

以前のレシピエディタはひとつの項目を選択するとモーダルが開き、入力を終えたら元の画面へ戻ってくるウィジェット型のUIでした。これはこれで、一つの項目に集中でき、シンプルな画面から徐々に要素を埋めていくため圧迫感が少ないというメリットがある一方、レシピを書き上げるまでのステップは多くなります。今回このウィジェット型から、一画面で入力・編集を完結できる「インライン型」のUIに変更し、各項目の入力の行き来をしやすくサクサク書き進められる構成に変更しました。

2.入力をアシストする

(A)作り方から材料の自動入力(B)タイトルから材料のサジェスト(C)分量の単位補完
f:id:sn_taiga:20190328220514p:plainf:id:sn_taiga:20190328220534p:plainf:id:sn_taiga:20190328220553p:plain

新しいエディタでは、レシピを書く人の入力をアシストする機能を充実させました。 料理の作り方を書いたら自動で材料欄が入力されます(A)。例えば「鮭を一口大にカットし、玉ねぎをスライスしておく」と書いたら材料欄に「鮭」と「玉ねぎ」が追加されます。 先にタイトルを入力すれば、その内容から材料を予測しサジェストもしてくれます(B)(こちらは研究開発部の機械学習の技術を用いて予測しています。予測のモデルについてはこちらの記事で紹介されていますので興味がある方はぜひ読んでみてください。) また、入力されている材料名から適切だと予測される単位を、分量の入力中にサジェストしてくれます(C) 。

このように「簡素化」という目的に対して「インライン型」「入力アシスト」という手段を用いた中、UIを設計する上での"デザインの方針"を定めました。どのような方針で、具体的にどういう工夫をしたのかについてここからはお話します。

入力量に圧倒されないように

ウィジェット型からインライン型にすることによって入力や編集の行き来がしやすくなる一方、画面内の情報量が増えます。元のUIをそのままインライン型に変更すると、画像のような文字量の多い入力フォームになり、ユーザーが画面を開いたときに「うっ、大変そう...」とレシピを書くハードルを高く感じてしまう危険性があるためこの方針を定めました。

f:id:sn_taiga:20190328220420g:plain:w300

具体的に行ったことをいくつか紹介します。

入力エリア、見出し、プレイスホルダーの同居

f:id:sn_taiga:20190328220720g:plain:w300

各項目の要素は入力エリア、見出し、プレイスホルダーの3つです。自分が今何を入力しているのかわかるように見出しは入力中も表示しておく必要があり、プレイスホルダーは「こういうものを書けばいいんだ」と理解するための手助けになります。ただ、これら3つを常に同時に表示しておく必要はないと考え、初めは入力エリアに重ねる形で見出しのみ表示し、フォーカス時に見出しを上側に移動させプレイスホルダーを表示するようにしました。これにより画面全体の文字量や高さを抑え圧迫感を減らすだけでなく、フォーカス前と後の状態をアニメーションでシームレスに繋ぐことで視覚的な負荷も軽減させました。

スケーラブルな入力エリア

f:id:sn_taiga:20190328221756g:plain:w300

レシピの紹介文やコツポイントといった長めの文章を書く箇所は、入力したテキストの行数に合わせて高さが変わるようにしました。高さを固定させる場合は、決めである程度の領域を確保しなければならず、テキストをあまり書かない人にとっては余分なスペースに、たくさん書く人にとっては狭いスペースになるのに対し、入力量に合わせて変化させることで必要十分な領域にすることができました。

入力中の項目に集中できるように

どこにもフォーカスしてないときは、入力されたテキスト以外は全体的に薄いグレーの配色で、最低限項目を認識できる存在感にし(入力可能な項目だとわかるように見出しの横に鉛筆アイコンを置いています)フォーカスした箇所のみをオレンジ色で強調し対象に集中しやすくしました。

f:id:sn_taiga:20190329091909g:plain:w300

元のUIをそのままインライン型にしたものと比べて画面全体の圧迫感が軽減しました。

自分の手で書き上げているように

入力のアシスト機能は便利になり得るものですが、タイミングや見せ方次第でユーザーの作業の妨げにもなりかねません。レシピを書く一連の流れに溶け込み余計な混乱を招かず、あくまでユーザー自身がアシスト機能を使いこなしながら「自分の手で書き上げた実感」を得られるようにとこの方針を定めました。

自動入力されたものをハイライト

f:id:sn_taiga:20190328224428g:plain:w300

作り方から自動入力された材料には背景色が付き、材料にフォーカスするか分量を入れるかすることで色が消えます。作り方はユーザー自身が書くものでもあり予測の精度はある程度高いためサジェストではなく自動入力という形で表示しているのですが、このような見せ方にすることで「自動で入力されたものだけど最終的にそれを採用するのは自分」だと思えます。

タイトルから材料のサジェストのタイミング

材料予測の通信が行われるのはタイトルを入力したときですが、このタイミングではサジェストせず、ユーザーが材料を入力する際に視界に入る位置に置いています。

タイトル入力直後にサジェスト(不採用)材料欄の上でサジェスト(採用)
f:id:sn_taiga:20190328222001g:plainf:id:sn_taiga:20190328224423g:plain

タイトル入力直後のサジェストは、自分のアクションに対するフィードバックが明確で驚きが生まれやすい一方、サジェストされた材料を追加したらそのまま足りない材料の入力へ移りたくなります。これはユーザーのエディタの使い方を制限することになっていて、レシピをタイトルから書き始める人もいれば作り方から書き始める人、材料から書き始める人、全部を一気に書いて最後に微調整する人など様々な人がいます。どのような書き方の人でもエディタの使い方を制限されることのないように、材料欄の上でサジェストするUIを採用しました。材料より先に作り方を書く人も上述の補完の恩恵を受けやすくなります。

f:id:sn_taiga:20190328224006g:plain:w300

また「使っている材料はこちらですか?」という投げかけのテキストと顔アイコンを置き、クックパッド側が提案してる風にすることで、ユーザーがサジェストの棄却をしやすいようにしました。合っている材料を追加したあとは右上の×ボタンからサジェスト自体を閉じることができます。

投稿にかかった時間11%減

今回、エディタの「簡素化」という目的に対し「レシピを書き上げるまでにかかった時間」を指標の一つとして置きました。ここまでの改善を行いリリースした結果、リニューアル前のエディタと比べて平均11%の減少率が見られました。画面全体の構成の変更やアシスト機能の追加によって、リニューアル前のエディタに比べて手間が少なくサクサクレシピを書き上げることができるようになったと言えます。

まとめ

具体的なUIを設計していく際に、目的を最大限達成するために何が重要なのか、ユーザーにどう感じてもらうのが良いのかを「デザインの方針」として言語化しておくことで、狙いから逸れることなくUIを設計できました。個人的にUIを作っている最中はついあれもこれもと横道に逸れがちなのですが、あらかじめ方針を定めておくことで、なぜやってるのか、何を狙っているのかでブレることなくUIの案出しや判断がしやすくなります。

最後に、クックパッドでは「より良い体験をユーザーに届けていきたい!」というメンバーを募集中です。興味を持っていただけた方は採用ページをご覧ください。


クックパッドが今、エンジニアとデザイナーを採用したい理由

$
0
0

こんにちは、クックパッドの冨永(@mamiracle__)です。

突然ですが現在クックパッドでは、サービス開発に携わるエンジニアとデザイナーの採用に注力しています!

しかし、社外の方とお話をしていると「クックパッドはひとつの完成したレシピサービスをやっている会社だよね。本当にエンジニアやデザイナーを積極採用しているのですか?」と聴かれることがよくあります。

答えは「YES!!!!!!」です。

わたしたちが積極採用をするのは、ミッションである「毎日の料理を楽しみにする」の達成に向けてサービスを生み出し、成長させるため。このことには大きな意義があるとクックパッドは信じていて、そのためにやるべきことが(レシピサービスでもそれ以外でも)たくさんあります。

そのことを皆さまに伝えるために、採用サイトを大幅にリニューアルしました。

今日はリニューアルに際してお伝えしたかった想いを、ブログに綴りたいと思います。ぜひ温かい眼差しでお読みいただけたら幸いです。

リニューアルした採用サイトはこちらです👇✨

クックパッド株式会社 | クックパッド株式会社 採用サイト

はじめに

食事が心とからだの健康をつくる、ということは誰もが認めていることです。

わたしたちにとって食事は、単なる栄養摂取の方法ではありません。一緒に食事をする家族や友人や、日常生活の中で接する人々とのつながり、さらに視野を広げてみれば地球環境を見直すことにまで密接に関係しています。(ある調査では、生産されたものの食べられていない食品ロス分だけでも、世界の温室効果ガスの8%に相当することがわかっているのだとか!)

わたしたちにとって「自分たちのための食事を料理すること」は、自分の意思(や食べる人のことを想って)で自分たちの食事をコントロールできる、とても身近で創造的で健康的な手段だとクックパッドは考えています。

ですが、料理することや食べることは日常生活に密着しすぎていて、まさにつくる・食べるその瞬間に大切さを改めて考えることは少ないかもしれません。わたしたち人間にとって不可欠に重要だけれど、考えずに「こなす」ことも容易なのが、料理や食べるという行為です。

クックパッドはそのことについて世界中の人々が、自然と考えたくなる世界、行動したくなる世界を目指しています。

「技術」で「料理の課題」を解決する

「料理」とひとことで言っても食材や道具などの生産、物流、流通、調理、食卓、洗い物、記録、実はさまざまなシーンが存在します。これを世界中の人々に向けて価値を届けようとすると、やりたいこと・やらなければならないことが溢れ出てくるのです!

いまは下記の三つの領域で、ミッションの実現に向けた挑戦をしています。

■つくり手を増やす

毎日の料理の課題を技術で解決し、つくるハードルを最小限に。つくる楽しさは最大限に。毎日の料理を楽しみにしし、つくり手を増やします。 f:id:mamiracle:20190402123438p:plain

各サービスの紹介はこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

関連するTechLife記事

■つくり手をつなげる

料理、道具、食材など、さまざまなつくり手をつなげることで、すべてのつくり手が継続的に収入を得ることができる仕組みをつくります。 f:id:mamiracle:20190402123444p:plain

各サービスの紹介はこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

関連するTechLife記事

■世界70億人のインフラへ

料理は、万国共通の習慣。だから、私たちの挑戦は国内にとどまりません。英国ブリストルに構えたグローバル本社を起点に、世界各国へとクックパッドを広げていきます。71カ国26言語にて展開しています。(2018年12月現在)

世界各地域のクックパッドはこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

英国ブリストルチームの開発者ブログ

sourcediving.com

一緒にやっていきませんか!

毎日の生活のちょっとしたワクワクと喜びの連続を生むために、わたしたちはサービスに触れていない時間も含めて、料理に関する全てを楽しみにする一連の体験をデザインします。

そして、料理の中にある様々な課題を、スケーラブルに解決するために技術の力を活用します。課題に向き合い、最善な手段を取り続けること。

そんな姿勢に共感してくださる方がいたら、ぜひ私たちに会いに来てください!

リニューアルした採用サイトには、お伝えしきれなかったことを詰め込んでいます。こちらもぜひご覧いただけたら幸いです。

Let’s make everyday cooking fun together!

info.cookpad.com

SRE Lounge #8 でスポンサー &登壇をしました

$
0
0

こんにちは。技術部 SRE グループの吉川 ( @rrreeeyyy ) です。

少し遅くなってしまいましたが、先日の 3/13 に行われた SRE Lounge #8の、 会場・フード・ドリンクスポンサーをクックパッドで行いました!

また、スポンサーだけでなく Cookpad Microservice Architecture Overviewというタイトルで登壇もしましたので、簡単に紹介させて頂きます!

SRE Lounge について

SRE Lounge は、UZABASE さんのSRE チームが中心となり発足した勉強会で、 現在は SRE 同士での情報共有や交流の場として有志の方により定期的に開催されています。

SRE Lounge は有志の方が開催しているため、会場の提供やドリンク・フードなどの支援を行うスポンサーを募集しており、 今回は、主催の方とのご縁もあり、クックパッドで会場提供ならびにドリンク・フードのスポンサーをさせてもらいました。

f:id:rrreeeyyy:20190403220609j:plain
乾杯の様子

各発表について

ソラコムAPIの裏側で運用チームは何をやってきたのか

最初は、株式会社ソラコムの五十嵐様より、「ソラコムAPIの裏側で運用チームは何をやってきたのか」というタイトルで発表がありました。 ソラコム様でのシステム構成や、内部での DevOps の取り組みや苦労についてお話されたあと、 SRE の採用について、各社の Job Description の分析についての調査結果を踏まえてお話されていました。

f:id:rrreeeyyy:20190403220725j:plain

資料はこちらです。

Cookpad Microservice Architecture Overview

次に、私、吉川 ( @rrreeeyyy ) が、 「Cookpad Microservice Architecture Overview」という題でお話させてもらいました。

クックパッドでのマイクロサービスへの取り組みに関して、 Cookpad Tech Kitchen #20などでもお話させて頂いてますが、 今回は、これらの全体像がどうなっているのかについて簡単に紹介しました。

発表は少し駆け足になってしまったのですが、この発表をベースに、懇親会で気になったことについて深い議論を交わすことができ、 非常に有意義な時間を過ごすことができたと感じています。

f:id:rrreeeyyy:20190403220833j:plain

資料はこちらです。

また、マイクロサービスへの取り組みのさらなる詳細は、 先ほど紹介した Cookpad Tech Kitchen #20などの資料も併せてご覧いただければと思います。

割れ窓理論をWebインフラの改善に活用し、チーム内の知識共有を促進している話

最後に、株式会社はてなの @hokkai7go 様より「割れ窓理論をWebインフラの改善に活用し、チーム内の知識共有を促進している話」というタイトルで発表がありました。

技術的負債や軽微な問題などを、Issue にしておき、週に 1 度、1 時間程度取って作業をしているそうです。 今回の発表にもある通り、こういった取り組みに対して実施してみてどうだったかの振り返りや改善をしっかり行えており、学びのあるお話でした。

f:id:rrreeeyyy:20190403220901j:plain

資料はこちらです。

おわりに

class SRE implements DevOpsと言われているように、 会社の事業や、チームとして実現したい信頼性のあり方によって SRE の振る舞いや、目指すべきゴールはある程度変わってくると考えています。

その際に、SRE Lounge のような場で、他社の SRE の方々と意見を交換し、どういったゴールを目指しているのか、 どういった取り組みを行っていて、どういった結果になったのかを知るのは、非常に有意義なものだと感じました。

SRE Lounge から派生して、テーマを決めてディスカッションを行う SRE Session #1というのも開催されるそうです。興味がある方は是非参加してみてください。

また、クックパッドでは SRE に限らず、SRE と一緒にサービスの信頼性に取り組んでくれる開発エンジニアも募集しています。 興味がある方は https://info.cookpad.com/careers/をご覧頂いたり、Twitter などでお声がけ頂ければと思います。

1日でSwiftコンパイラを作る!Swiftコンパイラインターンを開催しました

$
0
0

こんにちは、モバイル基盤部の @giginetです。

去る3月28日、Cookpad Spring 1day Internship 2019の一環として、Swiftコンパイラコースを開講しました。

最近のSwiftコンパイラ

近年、iOSエンジニアの間ではOpen Source Swiftがホットトピックとなっています。

ここ1年ほど、わいわいswiftcというSwift言語処理系に関する勉強会が盛り上がっていますし、 先日のtry!Swiftでは、参加者がSwift自体にcontributionするOpen Source Swiftワークショップが開かれました。

Swiftコンパイラに用いられているLLVMという技術は今、多くの言語処理系で利用されています。これを学ぶことで、さまざまな言語処理系に応用することができます。

このインターンは、Swiftコンパイラを例に、LLVMに触れ、コンパイラの動作を理解することを目的に構成しました。 後半のワークショップでは、実際にMinSwiftという簡易的なSwiftコンパイラをSwiftで開発してみます。

講義

まず最初の30分は講義パートです。Swiftコンパイラの構成を見ていき、LLVMの仕組みを学びます。

Swiftコンパイラ(swiftc)の構成

f:id:gigi-net:20190404183119j:plain

まず、Swiftコンパイラがどのようなフローを経て、動作するかを見ていきました。 コンパイラと一口で言っても、swiftcはパーサーや意味解析、中間言語など、様々な要素技術から構成されています。

1日でSwiftコンパイラ全てを理解することはできないので、今回はそれぞれの役割は説明するだけで留め、このインターンでは、現在、多くの言語処理系の要となっているLLVMに焦点を当てていきました。

LLVMを学ぶ

Swiftコンパイラのうち、LLVMとの橋渡しを行うIRGenに注目し、LLVMとは一体どのようなもので、どのように動作しているかについて学びました。

LLVMとは

LLVMは、コンパイラ基盤と呼ばれるもので、機械語の生成や最適化など、コンパイラに必要なものを共通化して作れるようにした仕組みです。

この仕組みを用いれば、一から作るよりは非常に少ない労力で汎用的なコンパイラが作成できます。 現に、Swiftのみならず、非常に多くのコンパイラがLLVM上で実現されており、LLVMについて学ぶことで多くの言語処理系について理解することができます。

LLVM IR

LLVM IR(LLVM Intermediate Representation)は、LLVMで使われる中間表現です。

どの言語であっても、最終的に適切なLLVM IRを生成することで、バイナリの生成や複数アーキテクチャへの対応、最適化などをLLVMの提供する仕組みに任せることができます。

LLVM IRは、ヒューマンリーダブルであるという特徴があり、例えば以下のような C のコードを例に挙げます。

int main(void) {
    return42;
}

このコードは、以下のような LLVM IR で表現することができます。

definei32@main() {
  reti3242
}

この講義では、LLVM IRの簡単な読み方や、LLVMの最適化がどのように動作するかを扱いました。

MinSwift

簡単な講義を経て、SwiftでSwiftをコンパイルするコンパイラ、MinSwift*1を製作しながら、コンパイラの構成や、LLVMの扱いについて学びました。

このワークショップは、LLVMが公式で提供しているLLVM Tutorialを参考に構成されており、ここで実装している架空の関数型言語Kaleidoscopeと同程度の表現力を持つコンパイラをSwiftで実装しました。

MinSwiftは以下のように動作します。

  1. Swiftのコードをパース
  2. Abstract Syntax Treeに変換
  3. LLVM IRを生成
  4. ビルドして実行オブジェクトを生成
  5. SwiftやC++からリンクして呼び出す

多くはSwiftコードのパーサーをSwiftで書いたり、LLVM IRの生成部分を実装しますが、他にも実装を通して、受講者は非常に多くのことを学ぶことができます。 例えば必要となったのは以下のようなトピックです

  • Swift Package Managerを使ったコマンドラインツールの開発
  • Swiftにおけるユニットテストの実行とTDD
  • Swiftによるパーサーの実装
  • LLVMSwiftを用いたlibLLVMの利用
  • LLVM IRの読み方
  • XcodeやLLDBの扱い方

ワークショップ

f:id:gigi-net:20190404183144p:plain

ワークショップは、予め用意されているユニットテストを通しながら、ドキュメントを参考に実装を進めていく形式となっていました。

f:id:gigi-net:20190404183201p:plain

テストケースが全て通過するように、1ステップずつ実装を進めていくと、最終的にSwiftコードからLLVM IRを生成し、LLVMを使ってコンパイルできるコンパイラ、MinSwiftが完成します!

最後まで実装を行うと、以下のようなコードをコンパイルすることができるようになります。少しは実用に耐えるコンパイラになったでしょうか?

funcfibonacci(_ x:Double) ->Double {
    if x <3 {
        return1
    } else {
        return fibonacci(x -1) + fibonacci(x -2)
    }
}

応用課題

最後に、それぞれ好きなテーマを探して、MinSwiftの改善に取り組んでもらいました。

例えば以下のような課題です。

  • 関数定義の拡張
  • 数値リテラルの改善
  • for文の実装
  • 変数の実装
  • 文字列リテラルの実装

課題を完了させて、独自の実装まで到達できた参加者は極僅かでしたが、一方で短い時間の中、上記のような言語機能を追加できた参加者もいました。

f:id:gigi-net:20190404183220j:plain

まとめ

普段Swiftを書き慣れていても、アプリ開発と言語処理系の開発では全くノウハウが違い、戸惑った方も多かったようです。

コンパイラインターンという割には、パーサーの実装量が多くなってしまったのは反省点です。

未経験の方には多少難しかったようですが、概ね好評を頂けたようで嬉しく思っています😊

クックパッドではSwiftコンパイラや言語処理系に興味があるエンジニアを募集しています。

*1:これはRubyコミッターの @mametterが行ったRuby処理系のワークショップ、MinRubyのオマージュとなっています https://techlife.cookpad.com/entry/2018/10/16/131000

クックパッドマートにおける実世界での配送を意識した注文の検証処理【連載:クックパッドマート開発の裏側 vol.1】

$
0
0

はじめに

こんにちは、買物事業部の勝間(@ryo_katsuma)です。 今日から5日間は、買物事業部のメンバーで連載記事を書かせていただきます。

買物事業部は、クックパッドの生鮮食品ECサービスの新規事業「クックパッドマート」の開発を行っている事業部です。 クックパッドマートのサービスについての説明や、立ち上げ期の舞台裏については昨年の長野によるエントリをご参照ください。

クックパッドマートは、iOS、Androidアプリのリリース、商品受け取り場所におけるスマートロックの設置、注文当日配送の実現など、 サービスをより多くの人に便利に使っていただくためのいろいろな新しい取り組みを行ってきました。 今回はこれらの取り組みについて多くの方にぜひ知っていただきたいと思い、連載形式で紹介させていただきます。

ちなみに明日以降は、以下のような内容を予定しています。

  • vol.2 クックパッドマートiOSアプリを楽しく新規開発した話
  • vol.3 1枚のラベルの向こうには、1人のユーザがいる
  • vol.4 クックパッドマートAndroidアプリ開発の裏側
  • vol.5 スマートロック開発におけるPDCA

注文の検証処理

vol.1の本稿では、クックパッドマートにおける注文の検証処理(validation)について紹介します。 注文の検証処理とは、クックパッドマート内での用語になりますが、その名の通りユーザーがカートに入れた商品について、「注文可能かどうか」を判定するための検証処理になります。

iTunes Storeのようなデジタルコンテンツの販売の場合、クレジットカードなど決済手段に問題がない限り「注文できるかどうか」の検証処理に複雑なケースはあまり多くなく、せいぜい販売上限数を考慮するくらいではないかと思います。 一方で、物流が絡むECにおいては、現実世界での非常に細かな制限が多く絡み、注文の検証処理には多くのことを考慮する必要があります。

ここからは、クックパッドマートにおける注文の検証において、どのようなことを考慮しているかをご紹介します。

前提

まず、クックパッドマートにおけるデータの関連性についてご紹介します。 説明を簡略化するために実際の概念とは一部異なるものもありますが、注文処理においては以下の概念が重要な登場人物になります。

product

  • いわゆる「商品」の概念
  • ユーザーが購入し、販売店や生産者の方に用意いただく食材を指す

location

  • クックパッドマートにおける「受け取り場所」の概念
  • ユーザーは常に1つの受け取り場所を指定している

delivery

  • location毎の注文締切、受け取り開始、受け取り終了などの「配送スケジュール」
  • たとえば「4/8 2:00注文締切」「4/8 16:00 ~ 23:00 商品受け取り可能」などのデータを持つ

order

  • いわゆる「注文」の概念
  • 注文毎に配送スケジュールのdeliveryに紐付き、各orderは複数のproductを持つ

データ間の関連性

rails的に表現すると、このような関連性を持っています。

classDelivery
  belons_to :locationendclassLocation
  has_many :deliveriesendclassProductendclassOrder
  has_many :products
  belongs_to :deliveryend

また、商品の集荷配送については、後ほど詳細を述べますが下記のような流れになります。

  • 販売店は注文情報に従い、コンテナに商品を入れて準備
  • ドライバーは指示書に従い、複数の販売店から商品が積載されたコンテナを集荷し、温度管理された配送資材(以下、シッパー)に入れる
  • ドライバーは指示書に従い、複数の受け取り場所でシッパーからコンテナを取り出し、冷蔵庫に設置する
f:id:ryokatsuma:20190405192518p:plain

注文の受付可能数

前述の通り、在庫の概念が無いデジタルコンテンツではなく、実在する販売店や生産者の方に用意いただく商品なので扱える数は有限です。 また、商品の多くはクックパッドマートだけではなく、実店舗でも販売されているので、その数には上限が設けられている必要があります。

そこで、商品には購入可能数が設定されてる必要があります。ここではsales_limit_per_dayとすると、購入可能数を超えているかどうかのチェックはこのようになります。

product.sales_limit_per_day < delivery.orders.where(product: product).count

これで購入可能数の確認は十分でしょうか?答えはNoです。

すべての配送場所を考慮

ある販売店の商品は、1つの受け取り場所だけではなく、N箇所の受け取り場所に配送されます。 言い換えると、あるdeliveryの配送日と同じ配送日の別のdeliveryが存在することになります。 たとえば、4/8配送分の販売店Aの豚肉は、受け取り場所1のユーザーで購入されていなくても、受け取り場所2のユーザーで購入されている可能性があります。

そこで、ある配送日における全配送場所のdeliveryを考慮すると、受付可能数のチェックはこのようにする必要があります。

deliveries = Delivery.find_by(date: delivery.date)
product.sales_limit_per_day < Order.where(delivery: deliveries, product: product).count

これで購入可能数のチェックはOKになりましたが、注文の検証としてはまだ不十分です。

受け取り場所のスペースを考慮

受け取り場所において、注文した商品は冷蔵庫の中のコンテナ内に設置されます。

f:id:ryokatsuma:20190405192532j:plain

受け取り場所の冷蔵庫設置面積は基本的に広げることはできないので、冷蔵庫の数は簡単に増やすことがはできません。言い換えると、受け取り場所における冷蔵庫内のコンテナの数も有限になります。 例えば、注文商品の種類が多く、コンテナに空きスペースが存在しない場合は、冷蔵庫に商品を設置することができなくなるので、配送しても冷蔵できない状態になるので注文を受け付けてはいけないことになります。

そのため、「注文しようとしている商品は受け取り場所のコンテナに設置できるかどうか」を判定する必要があります。 そこで、クックパッドマートでは、販売商品1つづつ簡易的に体積を測定し(縦x横x高さ)、注文しようとしている商品の体積は、受け取り場所のコンテナの総体積に収まるかどうかを確認しています。

f:id:ryokatsuma:20190405192553p:plain

実際は、コンテナは配送のオペレーションの観点で販売店や生産者ごとに分けられている(複数の販売店, 生産者の商品が1つのコンテナに混在することは無い)ので、冷蔵庫の中で

  • 空きコンテナを確保できるか
  • コンテナの中に空き容量があるか

を判定しています。

ContainerBox.find_available(
  shop: product.shop,
  delivery: delivery,
  capacity: product.volume
).any?

ちなみに、コンテナの空き容量確認で現在は体積を利用していますが、ここは議論の余地があると考えています。 たとえば、商品は上積み(= 商品の上に商品を載せる)を禁止させたほうが商品が痛むリスクを減らすことができるはずですが、「商品に上積みすれば体積的にはコンテナに積載可能」な状態になってしまいます。そのため、本質的には「商品の接地面積を測定し、その総数がコンテナの設置面積を超えなければ積載可能」とでもしたほうが良いと考えています。 ただ、実際は商品それぞれ接地面積を簡単に測定することは難しいため、オペレーションコストを考えて体積を利用して運用を行っています。

さて、これで注文の検証は十分でしょうか?

配送時に温度を考慮

クックパッドマートで扱う商品は、商品の品質を下げないようにするために「予冷品(肉や魚など)」「未予冷品(野菜など)」「常温品(パンなど)」と複数の温度カテゴリに属し、各温度帯の商品はそれぞれ別のシッパーで配送を行っています。

f:id:ryokatsuma:20190405171806j:plain

たとえば、商品は上積み(= 商品の上に商品を載せる)を禁止させたほうが商品が痛むリスクを減らすことができます。 しかし、現在の体積による空き容量確認では「上積みすれば体積的には積載可能」という判定になってしまいます。

また、パンも配送時に冷やしすぎると(約-2度〜5度)、小麦のデンプンがアルファ化して品質に大きな影響が出ることが知られています。 このように、商品の配送時にはその商品の「温度帯ごとに分けたシッパー」で配送することが、商品の品質保持の観点で必要になります。

一方で、各ドライバーが配送できるシッパーの積載量は有限です。 先の冷蔵庫の例と同じく、ドライバーも当日急に多く確保することは難しいので、あらかじめ確保したドライバーたちの車の積載量を超えるシッパーは配送できません。 つまり、1ドライバーが配送できるコンテナの総量も有限になるので、注文対象商品の温度カテゴリのシッパーに余裕があるかどうかを判定する必要があります。 注文対象商品の集荷配送を行うドライバーはあらかじめ決定できるので、このように書けます。

Shipper.find_available(
  driver: routing.driver,
  delivery: delivery,
  temperature_category: product.temperature_category
).any?

これらすべての確認を行うことで、「この商品は注文可能かどうか」が判定できます。なかなか道が長いですね。

まとめ

クックパッドマートにおける現在の注文の検証処理について解説を行いました。 実際はさらに細かな考慮がもう少しありますが、概ねこのような確認を注文処理の直前に行っています。 「かなり複雑すぎる処理をしているな、、」と思われた方もいるかと思いますが、実際に実装している僕もそう思います。

また、これらの処理はどんどんアップデートを行っています。 実際、温度カテゴリも最初は全く考慮せずにすべて冷蔵で配送していたものが、パンの扱いが出てきたことで2つの温度カテゴリになり、 夏の朝採れ野菜温度高すぎ問題に対応するために3つの温度カテゴリに対応しました。 このように現実に起きる問題に柔軟に対応するためは、的確にオブジェクト指向プログラミングで設計、実装を行う必要があり、ここは難しくも楽しいところでもあります。

一方で、このような複雑な処理も、おいしい食材をユーザーさんに確実に届けるために必要な条件だと考えています。 実際、リリースしてから半年近く経過していますが、冷蔵庫に設置できないなどの配送事故や、商品が傷んでいたといった事故は起きずに済んでいるので、引き続きおいしい食材を安全に届けるために技術によるサポートを行っていきたいと思います。

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートiOSアプリを楽しく新規開発した話【連載:クックパッドマート開発の裏側 vol.2】

$
0
0

こんにちは。
連載シリーズ2日目を担当します、クックパッド買物事業部 iOSアプリエンジニアの中山(@LimiterJP)です。
早いもので入社して一年が経ちました。

私は去年4月にクックパッドへ入社しました。
その後6月にアプリ開発を始め2018年9月に「クックパッドマート」のiOSアプリをリリースしました🎉

クックパッドマートは
生鮮食品をスマホアプリから簡単に注文することができる生鮮食品ECサービスです。
従来のネットスーパーや生鮮宅配サービスとは異なり、街の精肉店や鮮魚店などの販売店や地域の農家といった生産者など、小規模事業者や個人事業主が参加できるプラットフォームです。

今日はクックパッドマートのiOSアプリの立ち上げを爆速で行った舞台裏について、

  • 新規開発で大切にしたこと
  • 開発速度を上げるための考え方
  • 新規開発に特化した具体的な手法

の観点でお話します。

新規開発で大切にしたこと

ここでは、私が普段から新規開発を行う際に心がけていたことをいくつか紹介します。

開発スピードは大切

開発スピードは早ければ早いほど失敗から機能改善・成功までの速度も上がると考えます。

「ユーザーが実際にアプリを使用し、内容に共鳴・共感しリテンションを保てるか?」
という仮説検証を行う際、
「実際にアプリをリリースし、ユーザーが使用して操作してみないとわからない」
ということを認識する必要があります。

ユーザーがアプリをダウンロードしたときの印象にはレベルが存在すると考えます。
レベル1 使えない・よくわからないアプリ
レベル2 何に使うのか?を理解できる内容のアプリ
レベル3 なかなか使えるので使いつづけようと感じるアプリ
レベル4 神アプリ!シェアしよう!拡めたいと思うほどのお気に入りのアプリ

f:id:degikids:20190408134227p:plain

そのうち最低でもレベル3以上を目指さないと結果を出すことは難しいでしょう。
そして瞬間的にレベル4でも使い続けてもらえない内容だと別の問題にも悩むことになります。
アプリ開発って本当に難しい。

アプリリリース前にしっかり使用するシーン・ストーリー・コンセプト・カスタマージャーマップを組み立て入念な企画を立案します。
デザインを当ててみて機能を設計するなど頭の中でアプリを開発する。
ここまでの工程を私達のチームでは「論理できた」と呼んでいます。

ところがこの「論理できた」の状態でいざ実際にリリースしてみると
「実際のユーザーが継続的に使用することはなくニーズがずれて失敗する」
ことが多いのではないでしょうか?

これらは機能単位で起こっているなんてこともあります。
時間をかけて作った機能が使われないなんてことよくありますよね。

そのため、速く改善してレベル3, 4を目指せる状態を作ることでリテンション(継続率)を高められる状態になると考えます。

スピード重視とはいえ、雑に実装してバグを出してでもスピードを求めるとか
「コードレビューを全く行わないぞ、テストコードを書かないぞ!」とかそういうことではありません。
行う箇所を選定・判断することが大切です。
大切なのは効率化して工夫できることの選択肢を増やし開発スピードを向上させることです。
(後述する「開発速度を上げるための考え方」など)

チームメンバーとよく笑いよく話す

クックパッドマートのチームでは、基本的に誰かが面白いことを言っている現場なので笑いが絶えません。 (誰か反応するまで喋り続けるスタイルです!)

デザイナー、エンジニア、ディレクターなど職域を超えた人同士の雑談も多く、 Slackでやり取りすれば良いようなこともあえて喋るように、人間関係も良好で良いチームだと感じています。
そのおかげか、メンバーがどういう気持ちで仕事に向き合っているか、何を考えているのかも知ることができています。 業務で疑問に思ったことは身近な人に聞けば一瞬で解決することも多く、様々な仕事が円滑に進むので、会社に来ることに楽しさも感じます。

「普段から高い頻度で雑談ができるチームは強い。」 その結果、開発速度も改善速度も上がり、品質も高くなると考えています。   

圧倒的当事者意識

「圧倒的」とつけるとなんだかすごい感じがするのでつけてみたのですが当事者意識は非常に大切です。
自分が作っているサービスを使い倒す。使わないと問題点や改善点は理解できないですよね。
クックパッドマートで販売されているパンはとても美味しいのです🍞
エンジニア自身がアプリの課題を把握することで、自分が使いやすくするためには?という意識が自然に芽生えるものです。
よってアプリをしっかり普段使いすることは非常に重要です。

開発速度を上げるための考え方

ここでは、具体的にどのように開発速度を上げていくか、私なりの考え方について紹介します。

パレートの法則(2:8の法則)で物事を考える

パレートの法則とは全体の数値の大部分は全体を構成するうちの一部の要素が生み出しているという理論です。

例えば「アプリ利用者のうち8割は、全機能の2割しか使わない」とすると、

  • すべての機能のうち2割の重要な機能に集中する
  • 2割のユーザーしか使わない機能はほどほどに作る

などのヒントが得られそうです。

何が正しいかはチームで決めると良いでしょうし、実際にそのまま採用するのではなく頭で考える作業をします。 明らかに仕様頻度が少なく重要度の低いものに時間をかけない選択やphase分けをして最初のリリース時には実装しないなどの判断をすればいいと思います。

パレートの法則に当てはめることで、

  • 開発の優先順位をつけることでリリースまでの最短距離の見通しが良くなる
  • なにか取り掛かる際にこれらを意識することで開発スピードが目に見えて上がる

などの効果があると考えています。 やる事とやらない事をはっきりさせ、見通しをよくする事が大切ですね。

何事もphaseで分けた考え方を持つ

なぜならはじめから大きなものを作ろうとすると疲弊し、その他がおろそかになる可能性があります。
「段階を経て完成を目指す」というのでしょうか。
この時点ではその「作ろうとしているもの」がユーザーに受け入れられるかはわかりません。

そこで比較的工数がかかる実装はphaseで分けて考える場面があると思います。 具体的には複数の緯度経度と場所情報の情報を持つリスト構造の情報があったとします。

リスト構造の情報を地図上にピンが立てタップし直感的に俯瞰できると見やすいでしょう。 しかしリリース序盤だと受け取り場所の情報が少なすぎて 逆に見づらい上に寂しい感じもします。

f:id:degikids:20190408134619p:plain

そこでリリース時はただのリスト構造からタップして選択するUIを選択し、ロケーションが増えたらMapからピンをタップして選択するUIへ再構築するという判断が生まれます。

新規開発に特化した具体的な手法

ここでは、iOSアプリで新規開発を行う際の具体的な手法について紹介します。

同じコードを極力書かないようにコードスニペットは磨いておく

高い頻度で使用するコード記述をスニペットに登録しておけばいちいちGoogle検索したり、昔書いたソースコードを探してみたりすることなく、 使いたい時に正確な記述をサッと呼び出して使うことが可能です。

存在は知っていてもあまり使用されていないのが現実です。 私はどの言語を書くときでもIDEに付属しているコードスニペットを活用しています。

f:id:degikids:20190408142542p:plain

コードスニペットは定期的に磨いておくと開発効率が抜群に上がります。
iOSではXcodeのsnippetを活用しおり開発をしているとこれらは何度も登場します。

  • TableView, CollectionViewの最低限動くdelegateメソッド一式
  • GoogleMapの最低限動くdelegateメソッド一式
  • 地図からルート探索
  • 緯度経度のリスト構造と現在の位置情報からdistanceが近い順に並べ替え
  • UIActivityやShareなどのイベントハンドラ
  • 位置情報取得(パーミッションdelegate含む)
  • アラートやフルスクリーンの透過ダイアログ・モーダルウィンドウ各種
  • WebView・SafariViewの設置
  • チュートリアルなどのスライドをcollectionViewのpagingを使用して実装
  • カメラ起動 最低限動くdelegateメソッド一式
  • ライブラリから写真選択、アルバムから画像抽出
  • プッシュ通知の実装
  • GCD各種

登録時はCompletion Scopesでしっかり分類しcompletion shortcutは検索性を保った名称設計を心がけます。 たったこれだけでも、手を抜いてしまうと開発効率は落ちます。

人にもよりますが私の場合すべてのsnippet の completion shortcut prefixにsw_をつけています。(sw_はswiftを表していますObjective-C時代からの歴史的経緯もあります)

sw_から始まるものはすべてsnippetであるという分類ルールを持ち通常の補完と区別しやすいようにします。

f:id:degikids:20190408142347p:plain

普段SwiftでXcodeを使用してコーディングする時は

sw_xxxxで補完

または

 command + shift + 「l」

検索

↑↓cursor

Enter

の順でコードを書いていきます。

その結果、通常のコード補完よりも早く正確に書けるようになります。 初見のコードも一旦雑に書き、使い回せるようにコードレビューを繰り返し、磨き上がったらsnippetに丁寧に放り込みます。

チーム開発するときは
~/Library/Developer/Xcode/UserData/CodeSnippetsを共有しておくととても便利です。

例えばアプリ上でGoogleMapを設置しピンを立てて位置情報取得して画面でみて見ましょうか?という場面があったとします。 プロトタイピングツールでは難しい地図のモック作成の場面でも短時間で実装しディレクターやデザイナーと実際に地図を動かし良い悪いの議論することができます。この時点でプロトタイピングツールすら必要なく開発を進めることができます。

Xcodeのプロジェクトテンプレートを磨いておくと結果的にすべての開発効率が向上する

私はxcodeのプロジェクトテンプレートを利用しています。(具体的な用意の仕方はここでは割愛します)

ですが単純に自分の雛形プロジェクトテンプレートを用意しておけば良いと思っています。(新規プロジェクトで雛形アプリを作成)

以下のような機能を雛形の中に含めています。

  • 設定画面
  • TabBarController
  • お問い合わせ(WebView or SafariView)
  • アプリの使い方(WebView or SafariView)
  • プッシュ通知
  • 利用規約(同意の機構)
  • プライバシーポリシー
  • アプリバージョンの表記
  • ライセンスの表記
  • レビュー催促の仕組み
  • ログイン画面
  • Cloud FirestoreのCRUD

その他たくさん

まず上記を含んだ雛形のプロジェクトを複製し、その案件で必要ない機能は削除していきます。
上記の実装がXcodeから新規作成したときに既に実装されていたらどうでしょうか?
それはもう「強くてニューゲーム」です。

f:id:degikids:20190408140326p:plain

最初のスタートダッシュで大きな差がつきます。

Storyboardベースでつくる

Xcode6以前はコードベースで進めるほうが効率的でしたが、
iOS9.0以降からはStoryboardの分割が容易になりコードベースで作るより圧倒的に開発が楽になりました。

Storyboard Referenceの登場によりファイルの分割や関連付けも容易に。
以前と比べてConflictの可能性も低くくなったと感じます。

f:id:degikids:20190408140945p:plain

Storyboardを活用しデザイン確認が行えると開発が円滑に進む場面が多いように思えます。
例えばちょっとしたボタンの位置、配色などデザイナーと机を合わせて確認・議論ができます。簡単なモックもコードを汚さず作ることが可能です。

SwiftGenを活用する

SwiftGenとはXcodeで使用される画像イメージ、フォント、 カラー、segue等のリソース名を自動的に生成し型付を行ってくれるライブラリです。 https://github.com/SwiftGen/SwiftGen

Storyboardの遷移はコードで行っています。
理由として再利用率が高い画面を切り離して考えられるからです。

簡易的な値渡し例えばWebViewで開く情報を segueで渡すとシンプルかつ直感的に実現できます。

画面遷移では performSegueを使用しており、prepereで値渡しを行っています。
そのためにはSegueIDをStoryboardから設定する必要がありこれを手作業で管理するのは厳しい。
Segueのデメリットは「いろんな場所に設定が散らばっていて情報が隠れやすく流用がしづらい」ということだと思います。
コードからSegueIDやStoryboardを利用しようとした場合に、補完が効かないためハードコードをするか、手動で管理をする必要があります。

そこでSwiftGenの登場です。
簡単にStoryboardの名前からStoryboardのSegueのIDで名まで自動で生成し型付してくれます。

f:id:degikids:20190408142658p:plain

buildしなくても角丸とかボーダーなどを確認できるようになります。

class CustomView: UIView {
    @IBInspectable var customBool: Bool = false
    @IBInspectable var customInt: Int  = 0
    @IBInspectable var customFloat: CGFloat = 0.0
    @IBInspectable var customDouble: Double = 0.0
    @IBInspectable var customString: String = ""
    @IBInspectable var customPoint: CGPoint = CGPointZero
    @IBInspectable var customSize: CGSize = CGSizeZero
    @IBInspectable var customRect: CGRect = CGRectZero
    @IBInspectable var customColor: UIColor = UIColor.clearColor()
    @IBInspectable var customImage: UIImage = UIImage()
}

まとめ

クックパッドマートのiOSアプリはまだまだ発展途上で完成はしていません。
現在もコツコツと開発を進めており日を重ねるごとに利便性は向上しています。
使用できるエリアの皆さんは是非使っていただけると幸いです。ありがとうございます!
この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

1枚のラベルの向こうには、1人のユーザがいる【連載:クックパッドマート開発の裏側 vol.3】

$
0
0

こんにちは。クックパッドマート連載3日目を担当します、買物事業部エンジニアの今井(@imashin_)です。

去年の10月ごろから、生鮮食品ECサービスクックパッドマートの販売者向けサービスの開発を行っています。クックパッドマートを利用するのは、商品を買うユーザだけではありません。商品を販売する方々にも簡単に利用できるよう開発を進めています。

今回は、どのようにして商品を販売者からユーザまで届けられるように開発しているかを紹介します。

クックパッドマートではどうやって商品をユーザに届けているのか

まず、今どのように商品を届けているのか、商品の注文から受け取りまでの流れを紹介します。

発注

f:id:ima_shin:20190410170609p:plain

販売者は、四六時中クックパッドマートだけを利用しているわけではありません。これまで通りの生産、販売業務が忙しい中で、クックパッドマートも利用していただいています。

そのため、販売者に合わせた方法を開発し、負担にならないようにしています。

みなさんの近くにある精肉店、青果店を思い出してもらうとイメージが湧くかもしれないのですが、販売者はFAXや電話で注文を受けていることが多いです。必ずしもIT、インターネットに慣れているわけではありません。そのためクックパッドマートは、毎日FAXでの発注書の自動送信を行っています(FAX送信にはTwilioを利用しています)。一方でスマホから見たいという要望の販売者向けに、LINE WORKS経由でも発注書をPDFにて送付しています。利用者の多いLINEと同じUIを提供しているLINE WORKSを利用することで、利用障壁を大きく下げることができています。

仕分

販売者は発注で受けた商品の発送準備を行います。この準備段階で、ユーザが受け取り時に目印とするラベルの貼り付けを行います。

  • 07:00商品に貼るラベルを遠隔で自動印刷する
  • 07:00-14:00販売者が注文を受けた商品にラベルを貼る。 商品をコンテナごとに仕分けする

f:id:ima_shin:20190410170612p:plain

商品ラベルについても、完全に操作不要で発行できる構成で設置し、必要な時に必要なラベルを発行しています。また、商品へのラベル貼り間違いが発生せず仕分けが素早くできるよう、コンテナ別、商品別でラベルが発行されるようにソートしています。こうすることで、負担をかけないように工夫しています。

配送

  • 11:00-14:00配送員がコンテナを受け取りにくる
  • 14:00-17:00受け取り場所にコンテナを配送する

f:id:ima_shin:20190410170536p:plain

配送員は指定のコンテナを受け取り、冷蔵状態を保ちながら商品を集荷し、受け取り場所まで配送します。

受け取り

  • 17:00-ユーザが受け取り場所にて、自分の注文した商品をピックアップする

ユーザには配送が完了すると通知が送られます。受け取り場所に行き自分が注文した商品のIDを確認し、コンテナからピックアップしていきます。

どのようにして今の配送を作ったのか

クックパッドマートはまだまだ完成していないサービスです。今の配送フローがベストだとは考えていません。これからも日々、改良を続けていきます。

ですが、リリース当初の状態からはかなり改良されています。今回はどのように改良、開発を行っているかを商品の受け取りに必要なラベルの発行にフォーカスして紹介していきたいと思います。

クックパッドマートでは、基本的に

  • 初めから実装せず、頑張る運用からやってみる
  • 頑張る運用の知見を元に、プロトタイプを試験運用する
  • 利用者に当てたプロトタイプの知見を元に、スケール可能なプロダクトを作る

の段階を踏んでサービス開発を行っています。(ex サービスリリース初期の話

今回は商品ラベルの発行にフォーカスして、実際に行った開発をお伝えしたいと思います。

頑張る運用をやる

初期の段階ではコストをかけてでも(後々自動化可能な)配送を行えるのか検証を行いました。商品ラベルの発行は人の頑張りで次のような運用をしていました。

  • 注文の締めとともに社内に設置されている複合機でラベルを印刷する
  • 配送業者にラベルを配送してもらう

f:id:ima_shin:20190410170808p:plain
複合機でのラベル印刷

検証結果としては、商品へのラベル貼りを販売者が問題なく行えることを確認できました。加えて、ラベルに印字したIDを元にユーザが自分の受け取るべき商品をピックアップできることも確認できました。

プロトタイプを販売者にあてる

ラベルを毎日郵送するにはコストが莫大にかさみますし、スケールさせることも困難になります。そこで次の段階として、販売者にラベルを印刷してもらう方向でプロトタイプを作成しました。

安価に、素早く開発できることを基準に技術選定を行い、iPadとiOS用のSDKを提供しているラベルプリンターを採用しました。

  • ラベル発行用iPadアプリを開発し、ラベルプリンターにて印刷できるようにする
  • プロトタイプを店舗に設置し、試験運用してみる
    • ただし、問題発生時にはバイク便にてすぐラベルを届けられるようにバックアップを用意

f:id:ima_shin:20190410171717j:plain

f:id:ima_shin:20190410171833p:plain
ラベルの印刷フロー

このラベル発行アプリとiPadとラベルプリンターを販売者に提供することで、ラベルの配達をなくすことに成功しました。しかしながら、多くの問題点も浮き彫りとなりました。

  • ラベルプリンターの紙詰まりによる故障
    • 耐久性に特化したラベル発行機でないと長期の運用は保守が大変だった
  • 操作可能な画面は不要
    • 導入当初は、発注内容の確認や商品の情報入力をiPadからできるのではと思われたが、実際には設置場所の狭さや操作する余裕がないことがわかった
  • 通信環境の不良
    • iPadが安定してIPアドレスを払い出せない
    • iPadとプリンターの接続状態を安定させることが難しかった
  • OS、アプリの管理
    • 販売者の操作なしにOS、アプリを常に最新状態に保たせる仕組み、運用を作ることが難しかった
  • 販売者ごとのITリテラシーの差異
    • 必ずしも全ての販売者がiPadやプリンターの操作に慣れているわけではなかった

このように実際にプロトタイプで試験運用した結果、多くの問題点を洗い出すことができました。ラベルは商品を販売者からユーザに届けるために必要不可欠なものです。毎日必ず発行できる安定性を実現させる必要があります。

スケールできるプロダクトを作る

安定してサービスを運営するためには、ラベル発行に高い安定性が必要だということを認識することができました。また広くスケールをさせるためには、誰でも簡単に設置、管理できる必要があります。そこで、以下のような要件を元にスケール可能なプロダクトの開発を行いました。

  • 安定してラベルを発行できる構成と設計
    • 完全に遠隔でラベル発行をコントロールできる
    • ラベル発行が可能か把握するための死活を監視する
    • ラベル切れ等によるラベル発行不可能状態になる事前に検知する
  • 簡単に導入、運用できる設計
    • 電源を刺すだけ利用できる
    • 複雑な操作なしに運用できる

以上を満たすように開発を行い、つい2週間ほど前に新たな構成でプロダクトをリリースしました。

今の状態

では、今の構成がどのようなものかを紹介しようと思います。

ハードウェア

安定した稼働を実現するために、以下の機器でラベル発行機を組みました。

  • LTE ルーター UD-LT1/EX + SORACOM Air
    • 定期リブート
    • ネットワーク断絶時のリブート
    • Syslog
    • 外部ネットワークからの設定変更
    • SNTPによる状態監視
  • TSP743IIE3-24J1 JP
    • 通電すると常にONの状態に固定可能
    • 紙詰まりしにくい
    • ネットワーク経由でコントロール可能
    • カバーが開いている、紙が詰まっている、ラベルが切れかかっている等の状態を取得可能
    • SNTPによる状態監視
  • Raspberry Pi Model B+
    • デバッグ、キッティング、監視用

f:id:ima_shin:20190410171946p:plain
ラベル発行の構成

各機器の安定性、死活監視を利用することで、ラベル発行を安定して行うことができるようになりました。Raspberry PiでLTE通信を行うこともアイデアとしては挙がっていましたが、リリース速度を重視し、一旦既存のルータ製品を採用することにしました。

ソフトウェア

以上のハードウェアを稼働させるために、主に3つの開発を行いました。

star_ethernet

スター精密製プリンターを制御するiOSやAndroidのSDKは提供されていたのですが、サーバから直接利用するケースが少ないのか、Rubyはサポートされていませんでした。しかし、ソケット通信によるプリンターのプロトコルについて、細かな仕様が提供されていたため、Rubyからスター精密製プリンターを制御するgemを作成しました。

基本的にはTCPソケットで制御コマンドを送信し、プリンターを制御します。公開されている全てのコマンドをラップし、ラベル発行に必要なハンドリングを可能にしています。

例えば、文字を大きくしたりレイアウトを変えたりする、QRコードを印字する、線を引くといった印字内容の操作もこれを用いて行います。ラベル台紙のカットやラベル送り、ビープ音を出すこともできます。プリンターの細かな状態を取得することもできます。

f:id:ima_shin:20190410172040p:plain
https://www.starmicronics.com/Support/Mannualfolder/UsersManual_IFBD_HE0708BE07_EN.pdf

mart_server

プリンターへのラベル発行命令はECSから送信します。

mart_server(クックパッドマート全体を支えるRailsアプリケーション)に発行すべきラベルの情報を集約し、日次バッチにて発行するラベル情報をstar_ethernetを利用してプリンターに送信します。

バッチにはkuroko2を利用し、barbequeで各プリンターへのラベル発行ジョブの管理しています。何かしらのトラブルでラベル発行に失敗した時は、原因を調査しジョブを再実行することで全てのプリンターで確実にラベルが発行されるようにしています。またラベル残量が少なくなっていたり、紙詰まりの発生を検知しています。

mart_shepherd

配布端末、ネットワークの管理を新たなのアプリケーションとしてmart_shepherdに切り出しました。

mart_shepherdはSORACOMプラットフォームとの間に立ち、mart_serverからgRPC経由のリクエストに応じて端末の管理を行います。また、ルーター、プリンター、ラズパイ各端末との通信時にはプロクシを行い、通信路を確立します。

アセンブル

実際にこれらの構成を設置するためには、機器を一つの什器にまとめてコンパクトにする必要があります。また、電源を刺すだけで簡単に運用を開始できるようにすることを目指しました。

そこで、一つのボックスに全ての機器を収納し、プラグを刺すだけで全ての機器の電源がONになり、即座に運用状態になるようにしました。

弊社には、ハードウェアを加工できる「工房」と呼ばれるスペースが存在し、そこで全ての加工、組立を行いました。

f:id:ima_shin:20190410172304j:plain
加工中の様子

f:id:ima_shin:20190410172230j:plain
加工済みのボックス

f:id:ima_shin:20190410172532p:plain
アセンブル済のボックスとプリンター

改善点

以上のように、安定して稼働するプロダクトを完成させることができました。2週間運用している限りでは、何かトラブルが発生しても遠隔で復旧することに成功していて、ラベルの発行ができない問題にぶつかったことはありません。

しかし、まだ改善点は残っています。

  • 低コスト化、小型化
  • 輸送可能な構成、耐衝撃性
  • 熱制御

これらを実現するために、引き続き開発を行っています。

まとめ

たかが商品のラベル1枚と思いがちですが、ラベルが発行されないとユーザーに正確に商品を届けることができません。1枚のラベル発行に失敗すると、1人のユーザが料理を作れない状態に陥ってしまいます。

そのようなことが決して起きないよう、たかがラベルの発行であっても真剣に開発に取り組んでいます。

これからもクックパッドマートは、素早いサイクルでの開発の元、安定したサービスの提供と、スケールを実現していきます。

この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートAndroidアプリの画面実装を最高にした話

$
0
0

こんにちは。 連載シリーズ4日目を担当します、買物事業部 Androidエンジニアの門田(twitter: @_litmon_ )です。

↓↓↓以前の3日分のエントリはこちらから参照ください↓↓↓

買物事業部では、クックパッドの生鮮食品ECサービス「クックパッドマート」の開発を行っています。 今日は、先日リリースしたばかりのクックパッドマートAndroidアプリを開発する上で、画面実装の工夫について紹介しようと思います。

クックパッドマートAndroidアプリはこちらからダウンロードできます。ぜひ実際に触りながら記事を読んでみてください。 play.google.com

クックパッドマートAndroidアプリの画面実装

クックパッドマートAndroidアプリの主な画面は、大きく分けて3つに分類されます。この分類は、多少の違いはあれど一般的なAndroidアプリに対しても同様のことが言えるのではないでしょうか。

  • 一覧画面: データのリストを一覧表示する画面
  • 詳細画面: 一覧画面の特定のデータに対して詳細を表示する画面
  • 入力画面: データを登録したり追加したりするために入力を行う画面
一覧画面詳細画面入力画面
f:id:litmon:20190411123251p:plainf:id:litmon:20190411123345p:plainf:id:litmon:20190411123414p:plain

現代のAndroidアプリ開発において、一覧画面にはRecyclerViewが使われるのが一般的です。RecyclerViewは、同一のレイアウトを複数持つ一覧画面において非常に高いパフォーマンスを発揮するViewですが、AdapterやViewHolderなど実装するものが多く、若干扱いにくいのが難点です。

詳細画面の実装に関しては、スマートフォンのディスプレイは縦に長く、スクロールの方向も縦になるアプリが多いため、 ScrollViewやNestedScrollViewを使ってその中にレイアウトを組むのが一般的だと思います。

入力画面には、EditTextやCheckBoxなどを利用して入力欄を用意すると思います。また、入力項目が多くなった場合には詳細画面同様にScrollViewなどを使ってスクロール出来るように実装することが多いのではないでしょうか。

クックパッドマートAndroidアプリでは、上の例に漏れず一覧画面ではRecyclerViewを使い、詳細画面ではScrollViewを使うというスタイルを取っていたのですが、 このレイアウト手法で開発を進めていくのが大変になっていきました。 特に、詳細画面の実装をScrollViewで行っていくことに関して非常に苦しい思いをした例を紹介します。

レイアウトエディタでのプレビューが活用しづらい

ScrollViewを使って縦に伸びるレイアウトを組む場合、縦に伸びれば伸びるほどレイアウトエディタのプレビュー機能が使いにくくなっていきます。 また、レイアウトファイルも肥大化し、非常に見通しの悪い実装になりがちです。

詳細画面の実装がActivity, Fragmentに集中して肥大化しやすい

RecyclerViewを使うと、Viewの実装の大半はViewHolderクラスに分離することが出来ます。 しかし、詳細画面ではScrollViewを使っているため、データをViewに紐付ける処理をどうしてもActivity, Fragment内に書くことが多くなります。 DataBindingやMVVMアーキテクチャなどを使ってViewの実装をActivity, Fragmentから分離する手法などもありますが、プロジェクトによってはあまり適さないケースもあるでしょう。 また、RecyclerViewを使う一覧画面と実装差異が出てしまい、処理の共通化などが難しくなってしまいます。

なにより実装していて苦しい

詳細画面のような複雑なレイアウト構成を1つのレイアウトファイルに対して上から順に実装していくのは、精神的にも苦しいものがあります。 長くなればなるほどレイアウトエディタ, XML両方の編集作業が難しくなっていくため、細かい単位でレイアウトを分割できるRecyclerViewのような仕組みが欲しくなってきます。

includeタグ?知らない子ですね……

すべての画面をRecyclerView化する

そこで、RecyclerViewをうまく使うことで詳細画面もうまく組み立てることが出来るのでは?と考えました。RecyclerViewは、レイアウトを行ごとに分割して作成することが出来るし、ViewHolderへViewの実装を委譲出来るため、ActivityやFragmentの肥大化を防ぐことが出来ます。 ただ、RecyclerViewの実装には、AdapterとViewHolderの実装が必要で、特に複雑な画面になるほどAdapterの実装が面倒になっていきます。

class DeliveryDetailOrderItemsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    overridefun getItemCount(): Int =
        1 + 1 + items.size + 1overridefun getItemViewType(position: Int): Int {
        if (position == 0) {
            return R.layout.item_delivery_detail_header_label
        }

        if (position == 1) {
            return R.layout.item_delivery_detail_order_item_note
        }

        if (position == itemCount - 1) {
            return R.layout.item_delivery_detail_order_item_footer
        }

        return R.layout.item_delivery_detail_order_item
    }
}

RecyclerViewで受け取り詳細画面を実装したときの一部を抜粋してきました。 表示するpositionに応じてそれぞれのitemViewTypeを変える必要があるのですが、全く直感的ではなく、頭を使って実装する必要があります。 また、Viewを追加したいという変更があったときに、他の部分にも影響が出る場合があるので、保守性も高くありません。

すべての画面でこのような複雑なRecyclerView.Adapterを実装するのは気が滅入りますし、現実的ではありません。 しかも、読み込んだデータに応じて表示を変えなければいけない、となるとより面倒になるのは必至です。 そのため、RecyclerView.Adapterの実装を簡素に行うためのライブラリを導入することにしました。

Groupieを使う

RecyclerView.Adapterの面倒な実装を便利にしてくれるライブラリは巷にいくつかありますが、今回はGroupieを使うことにしました。 同様の仕組みを持つEpoxyというライブラリも候補に上がっていましたが、判断の決め手となったのは以下の点でした。

  • EpoxyはannotationProcessorを使ったコード生成機構が備わっており、DataBindingと連携させるととても便利だが、クックパッドマートAndroidアプリではDataBindingを使っておらずオーバースペックだった
  • GroupieはRecyclerView.Adapterを置き換えるだけで使えるので非常に簡素で、今回のユースケースにマッチしていた

例えば、Groupieを使って一覧画面のようなデータのリストを表示させるために必要なコードは以下です。

dataclass Data(val name: String)

class DataItem(valdata: Data) : Item<ViewHolder>() {
   overridefun getLayoutId(): Int = R.layout.item_data

   overridefun bind(viewHolder: ViewHolder, position: Int) {
       viewHolder.root.name.text = name
   }
}

val items = listOf<Data>() // APIから返ってきたリストとするval adapter = GroupAdapter<ViewHolder>()
recycler_view.adapter = adapter

adapter.update(mutableListOf<Group>().apply {
    items.forEach {
      add(DataItem(it))
    }
})

たったこれだけです。とても簡単ですね。

詳細画面の場合、データの有無で表示する/しないを切り替える必要があったりしますよね。 例えば、クックパッドマートAndroidアプリのカート画面では、カートに商品が追加されていない場合と追加されている場合で表示が異なります。

f:id:litmon:20190411123600p:plain:h300f:id:litmon:20190411123620p:plain:h300

このようなレイアウトになるようにRecyclerView.Adapterを自前で実装しようとすると、 getItemViewType()メソッドの実装に苦しむ姿が簡単に想像できますね……絶対にやりたくありません。

しかしこれも、Groupieで表現すると以下のように簡単に表現することができます。簡略化のため、表示が変わる部分のみを表現します。

class Cart(
    val products: List<Product>
) {
    class Product
}

class CartEmptyItem : Item<ViewHolder>() { /* 省略 */ }
class CartProductItem(val product: Cart.Product) : Item<ViewHolder>() { /* 省略 */ }

val cart = Cart() // APIから返ってきたカートオブジェクトval adapter = GroupAdapter<ViewHolder>()
recycler_view.adapter = adapter

adapter.update(mutableListOf<Group>().apply {
    if (cart.products.isEmpty()) {
        add(CartEmptyItem()) // 商品が追加されていない旨を表示する
    } else {
        cart.products.forEach {
            add(CartProductItem(it)) // カートの商品をリストで表示する
        }
    }
})

非常にコンパクトな実装に収まります。 前述の例とあわせて見ると、getItemViewType()を実装するのに比べて直感的になることも理解できると思います。

LiveDataと組み合わせて使う

LiveDataと組み合わせて使う場合もとても簡単です。Fragment内で使うことを例に挙げてみましょう。

class CartFragment : Fragment() {

    class CartViewModel : ViewModel {
        valdata = MutableLiveData<Cart>()
    }

    val viewModel by lazy {
        ViewModelProviders.of(this).get<CartViewModel>()
    }

    val adapter = GroupAdapter<ViewHolder>()

    overridefun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_cart, container, false)
    }

    overridefun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        recycler_view.adapter = adapter

        viewModel.data.observe(this, Observer {
            it?.let { cart ->
                adapter.update(mutableListOf<Group>().apply {
                    cart.products.forEach { product ->
                        CartProductItem(product)
                    }
                })
            }
        })
    }
}

非常に簡単ですね。 Groupieはupdate時に内部でDiffUtilsを使って差分更新を行ってくれるため、APIリクエストを行った結果をLiveDataで流すだけで簡単に更新が出来ます。

その際、GroupieのItemに対して以下の2点を見ておく必要があります。

  • id が同一になるようになっているか
  • equals が実装されているか

idの設定は、getId()メソッドをoverrideすると良いでしょう。 もしくは、Itemクラスのコンストラクタ引数にidを渡すことも出来ます。

equals()メソッドの実装は、Kotlinならばdata classで簡単に実装することが出来ます。 また、引数を持たないようなItemで、特に中の内容も変わらないような場合は自前で実装してしまっても良いでしょう。

dataclass CartProductItem(val product: Cart.Product) : Item<ViewHolder>() {

    overridefun getLayoutId(): Int = R.layout.item_data

    overridefun getId(): Int = product.id

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        viewHolder.root.name.text = name
    }
}

// idをコンストラクタで指定class CartEmptyItem : Item<ViewHolder>(0) {

    overridefun getLayoutId(): Int = R.layout.item_data

    overridefun hashCode(): Int = 0// 同じItemなら同じと判定して良いoverridefun equals(other: Object): Boolean =
        (other instanceOf CartEmptyItem)

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        // ignore
    }
}

これらの設定がうまくいっていない場合、更新されたときに無駄なアニメーションが走ってしまうため、できるだけ全てのItemに実装しておくことをオススメします。

実際にクックパッドマートAndroidアプリでは、ほぼすべての画面がこの構成を取って実装していて、画面回転時やFragmentのView再生成にも問題なく状態を再現してくれるのでとても便利な構成になっています。

Groupieを使うことで良くなった点

Groupieを使うことで、RecyclerViewの面倒な実装を簡略化でき、かつすべての画面の実装を定型化することが出来ました。 これにより、以下のような効果が生まれました。

  • Fragmentの実装をすべての画面でほぼ定形化出来るため、精神的に楽に実装できるようになり、かつ処理の共通化が簡単になった
  • RecyclerView.Adapterに比べて、複雑なレイアウトを組むのが非常に簡単なので、実装工数を大幅に削減することが出来た
  • レイアウトが強制的にItem単位になるため、シンプルにレイアウトを作成することが出来るようになった

1つ目の処理の共通化には、例えばエラー画面が挙げられます。 読み込みエラー時の画面表示をGroupieのItemで用意することで、非常に簡単に全ての画面で同一のエラー画面を用意することが出来ます。

class ErrorItem(val throwable: Throwable): Item<ViewHolder>() {
    /* 省略 */
}

apiRequest()
    .onSuccess { data->
        adapter.update(mutableListOf<Group>().apply {
            add(DataItem(data))
        })
    }
    .onError { throwable ->
        adapter.update(mutableListOf<Group>().apply {
            add(ErrorItem(throwable))
        })
    }

また、アプリ内のItemの間に表示されている罫線も、RecyclerViewのItemDecorationを使うことでアプリ全体で簡単に共通化することが出来ました。 RecyclerViewにLinearLayoutManagerとあわせてdividerを設定することがとても多かったため、RecyclerViewに以下の拡張関数を実装して使うようにしています。

fun RecyclerView.applyLinearLayoutManager(orientation: Int = RecyclerView.VERTICAL, withDivider: Boolean = true) {
    layoutManager = LinearLayoutManager(context).apply { this.orientation = orientation }
    if (withDivider) {
        addItemDecoration(DividerItemDecoration(context, orientation).apply {
            ContextCompat.getDrawable(context, R.drawable.border)?.let(this::setDrawable)
        })
    }
}

Groupieで難しかった点

Groupieを使っていて、難しかった点もいくつかあります。 例えば、受け取り場所の詳細画面には地図を表示しているのですが、MapViewにはMapFragmentをアタッチする必要があります。 単純にaddするだけの実装だと、スクロールして戻ってきたときにクラッシュしてしまうので、unbind時にFragmentを取り除く必要がありました。 苦肉の策ですが、現状は以下のような実装になっています。

dataclass SelectAreaDetailMapItem(
    val item: Location,
    val mapFragment: SupportMapFragment,
    val fragmentManager: FragmentManager
) : Item<ViewHolder>() {
    overridefun getLayout(): Int = R.layout.item_select_area_detail_header

    overridefun getId(): Long = layout.toLong()

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        val markerPosition = LatLng(item.latitude, item.longitude)
        fragmentManager.beginTransaction()
            .add(R.id.item_select_area_detail_map, mapFragment)
            .commit()
        mapFragment.getMapAsync { map ->
            map.addMarker(MarkerOptions().position(markerPosition))
            map.moveCamera(CameraUpdateFactory.newLatLng(markerPosition))
            map.setMinZoomPreference(15f)
        }
    }

    overridefun unbind(holder: ViewHolder) {
        super.unbind(holder)
        fragmentManager.beginTransaction()
            .remove(mapFragment)
            .commit()
    }
}

すべての画面でGroupieを使うことで実装が簡単になった、アプリ全体で処理を共通化出来たというメリットはありましたが、こういう風に扱いに困るケースもあるため、用法用量を守って正しくお使いください。

まとめ

  • Androidアプリ開発において主要な画面はだいたいRecyclerViewで表現できる
  • Groupieを使うと実装も簡単になって最高
  • 難しい画面もあるので適材適所で使い分けよう

おしらせ

4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催します!!! cookpad.connpass.com

そこでは、今回語らなかったAndroidアプリの技術的な部分を紹介していこうと思います。 実際のソースコードも見せたり……あるかもしれませんね。 ぜひぜひ!!ご興味のある方は参加お待ちしています!


新規事業のIoTプロダクト開発に必要なこと【連載:クックパッドマート開発の裏側 vol.5】

$
0
0

クックパッド 買物事業部の篠原 @shanonimです。社内の新規事業「クックパッドマート」でエンジニアをやっています。

このエントリは、連載シリーズ【連載:クックパッドマート開発の裏側】の第5回目です。本日が最終回となります。 以前のエントリはこちらからご参照ください。

今回はクックパッドマートのIoTプロダクト開発について、開発の概要、これまでの歴史、そしてプロダクトのこれからについてご紹介したいと思います。

このエントリに書いてあること

  • クックパッドマートについて
  • IoTプロダクトの開発経緯
  • 開発の歴史
  • 開発を通して得た学び

このエントリで紹介しないこと

  • IoTプロダクトを構成する個々のデバイスに関する詳しい説明

クックパッドマートの仕組み

クックパッドマートは、生鮮食品に特化したECサービスです。 商品を自宅に直接届けるのではなく、マートステーションと呼ばれる生鮮宅配ロッカー(冷蔵庫)に商品を配送します。
マートステーションは街の様々な場所に設置されており、ユーザーは自分の注文した商品を自分で取りに行くことができます。

f:id:shanonim:20190412174027j:plain
クックパッドマートの仕組み

クックパッドマートのIoTプロダクト

現在、クックパッドマートには大きく2つのIoTプロダクトがあります。

一つは、上図「② 商品の準備」に必要なラベルプリンターです。 こちらについては、2019/4/10に投稿された @imashin_の記事に詳しい説明があります。

techlife.cookpad.com

もう一つは、上図「④ 商品の受け取り」に関連するスマートロックです。 クックパッドマートのアプリからマートステーションの鍵を操作することで、商品を受け取るユーザーだけがマートステーションにアクセスできる仕組みを作っています。

f:id:shanonim:20190412174146j:plain:w400

このエントリでは、スマートロック開発についてご紹介します。

スマートロックの必要性

マートステーションには大きく、

  • 有人ステーション
  • 無人ステーション

の2つの形式があります。
前者は、街なかのドラッグストアや酒店・カラオケ店など、有人店舗の店内に設置されています。現在オープンしているマートステーションはすべて有人ステーションです。

これに加えて、よりユーザーにとって便利な選択肢を増やすために計画しているのが無人ステーションです。

  • 例えば、駅の構内に無人ステーションがあれば、帰宅途中に最寄り駅で生鮮食品をピックアップすることができます。
  • 例えば、マンションの共用部に無人ステーションがあれば、建物の外に出なくても食品を受け取ることができます。

無人ステーションが解決しなければいけない問題の一つに、セキュリティ問題があります。 有人監視のない無人ステーションの場合、悪意ある第三者による商品へのいたずらや盗難といったリスクを否定できません。「特定の人だけがマートステーションにアクセスできる」仕組みが必要です。

そこで、無人ステーションの本格的展開に先駆けて、マートステーション向けのスマートロックを開発することになりました。

開発の方向性

初めからスマートロック付きの冷蔵庫を買ってきて導入できれば話は早いのですが、私たちのニーズにマッチした商品はなかなか見つかりませんでした。 そこで、既存の冷蔵庫を改修してスマートロックを開発することにしました。

f:id:shanonim:20190412174328j:plain
スマートロック実装前のマートステーション

改修と言っても勝手に冷蔵庫を分解して改造することはできません。
今回の開発は冷蔵庫を分解・改造せずに、スマートロックの機構だけを外付け実装するという条件で進めています。

スマートロックの仕組み

鍵の仕組みと一言で言っても、世の中には様々な方法が存在します。物理鍵を鍵穴に差し込んで回すもの、ダイヤル式の番号を合わせるもの、カードをかざして開け閉めするもの...
マートステーションに必要な鍵の条件は、次の2つでした。

  • 遠隔で操作できる仕組みが作れること
  • 物理的な施錠能力が高いこと

条件に合う仕組みを探した結果、電磁錠に辿り着きました。

f:id:shanonim:20190412174434j:plain

電磁錠は、電磁石の特性を利用した鍵です。金属と電磁石を重ねて設置して通電すると、磁石の力でロックされます。 (通電時施錠型と通電時解錠型がありますが、マートステーションでは前者を使用しています。) 通電状態を遠隔でコントロールできれば、施錠状態をコントロールすることができそうです。
また、物理的な施錠能力も非常に高く、大人が思いっきりドアを引っ張ってもビクともしないくらいの強度があります。

スマートロックは、この電磁錠を中心として開発が進められています。

開発の歴史

現在進行中の開発も加え、これまで5つのプロトタイプを製作してきました。それぞれの世代にはコードネームとして貝の名前がつけられています。(貝の甲羅が開いたり閉じたりする様子が鍵の開け閉めを連想するという @_litmon_のアイディアです。)

第一世代: シジミ

f:id:shanonim:20190412174717j:plainf:id:shanonim:20190412174642j:plain

鍵の制御用デバイス 鍵の切り替え用デバイス
電磁錠(通電時施錠型) M5Stack Grove - Dry-Reed Relay

冷蔵庫の上部に電磁ロックを取り付けています。アプリからインターネットを介して制御装置(M5Stack)に解錠コマンドを送ると、冷蔵庫の扉を開けることができます。

課題

  • 熱問題: 電磁錠を連続可動させると、本体が熱を持ってしまう問題がありました。機器が壊れるほどの温度ではありませんが、連続稼働に不安があります。
  • 耐久性: 鍵自体の耐久性に問題がありました。鍵がかかっていることを知らずに冷蔵庫の扉を開けたユーザーが鍵を壊してしまう事件がありました。

第二世代: アサリ

f:id:shanonim:20190412174743j:plainf:id:shanonim:20190412174801j:plain

部品構成は第一世代と同じですが、電磁錠の設置場所を冷蔵庫上部から冷蔵庫内部に変更しています。 これにより、前世代の熱問題を解決することができました。

課題

  • 耐久性: 冷蔵庫内部に固定した電磁錠が時折ずれてしまい、うまく鍵が閉まらない事象が頻発しました。

第三世代: ハマグリ

f:id:shanonim:20190412174830j:plain

第三世代では、電磁錠の設置場所が冷蔵庫下部に変更されています。 冷蔵庫の製造メーカーに設置方法を相談したり、スマートロックの先行事例を研究したり、様々な試行錯誤の末にようやく落ち着いた仕様でした。

第四世代: ホタテ

f:id:shanonim:20190412174903j:plain:w400

現在マートステーションの一部で稼働しているスマートロックは、この第四世代です。 この世代では、物理鍵以外のデバイスを刷新しました。

鍵の制御用デバイス 鍵の切り替え用デバイス
電磁錠(通電時ロック型) Androidタブレット スマートプラグ

鍵の制御用デバイスの刷新

これまで鍵の制御に使っていたマイコン(M5Stack)には2つの課題がありました。

  • 安定性: 長時間の連続稼働が難しく、物理的な故障リスクが高い。
  • 視認性: 前面のディスプレイが小さく、鍵がかかっているのかいないのか分かりにくい。

これらを解決するために、マイコンをAndroidタブレットに変更しました。

鍵の切り替え用デバイスの刷新

これまで、物理鍵の制御(電流の制御)は自分で手作りしたデバイスを使っていました。動作的には問題ないのですが、一般的に市販されているデバイスと比べてみると、安全性や耐久性にやや不安が残ります。

f:id:shanonim:20190412174931j:plain:w600
手作りしていた鍵の切り替え用デバイス

そのため、このデバイスを市販のスマートプラグに変更しました。

第五世代: サザエ

第五世代は、現在開発中の次世代プロダクトです。 デバイスの耐久性をより強固にしたり、プロダクトの状態を外部から常時監視できる仕組みを作りたいと思っています。

新規事業のIoTプロダクトの開発に必要なこと

アイディアを片っ端から試す

今回のスマートロック開発では、プロトタイピングのために必要な資材を買って、試して、ダメなら壊す、を高速で繰り返してきました。 日々手探りの連続ですが、思いついたアイディアを片っ端から試していくことが大切です。
思いついた時点では微妙だなーと思うアイディアも、実際に作ってみると案外悪くなかったり、逆にこれでいける!!と思ったアイディアも作ってみるとダメだったりします。
初期の開発においては「雑でもいい、まだプロトタイプなんだから」と割り切ってとにかく手を動かして学びを得ていくのが一番の近道だと思います。

実際に使ってもらう

実際にプロトタイプが形になったら、とにかくいろんな人に使ってもらうことをおすすめします。 これはソフトウェアのサービス開発におけるユーザーインタビューと同じ概念かもしれません。

マートステーションはクックパッド社内にも設置しており、スマートロック開発の際はここを物理ステージング環境として使いました。

f:id:shanonim:20190412175017j:plain

今もユーザー(クックパッド社員)に意見をもらいながら、日々改善を繰り返しています。

まとめ

マートステーションのスマートロック開発を通して得た「IoTプロダクト開発に必要なこと」をご紹介しました。 最初から完璧なIoTプロダクトを作ることはほぼ不可能です。仕様の検討や実装に時間がかかってしまうため、現実的ではありません。
必要なのは、「次世代版までの時間を稼ぐ現世代版を開発する繰り返し」だと思っています。

  • まずは1~10個世代を作る
  • その世代での学びを、次の10~100個世代の開発に活かす
  • さらにその先の運用に耐える100~1000個世代世代を開発する

現在のマートステーションもまだ完成形ではありません。これからも着実かつ高速に、この繰り返しを続けていきたいと思っています。

cookpad mart Meetup

このエントリでお伝えしきれなかった技術的な話やもっと深い話を、4/24に開催されるミートアップでご紹介する予定です。 cookpad.connpass.com

ご興味のある方はぜひご応募ください!

クックパッド一同は、RubyKaigi 2019でみなさんにお会いできることを楽しみにしています!

$
0
0

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

先月公開した【RubyKaigi 2019 参加者に捧ぐ】福岡で起業した男が本気で書いた福岡グルメまとめは見ていただけましたでしょうか? 私は、ブログへの掲載はありませんでしたが、福岡の餃子には牛肉をタネに使っているお店があるとの情報を入手したので、食べに行くぞと意気込んでいます。

クックパッド株式会社は、2019年4月18日(木)〜20日(土)に福岡にて開催される RubyKaigi 2019に、Ruby Committers’ SponsorとWi-Fi Sponsorとして協賛します。

Wi-Fi Sponsorに関しては、調達・設計・構築・運用などを、昨年に引き続き @sorahが担当しております。

クックパッドに所属する4名が登壇し、 @asonas@sorahが運営として関わってくれています。

本ブログでは、登壇する社員のセッションのスケジュールや、ブースで行う登壇者へのQ&Aタイム、Cookpad Daily Ruby Puzzles など限定企画の紹介をいたします。RubyKaigi 2019に参加する弊社メンバーは内定者を含め約30名! みなさまと交流することを楽しみにしています。

参加社員一覧

@mirakui, @tapster, @kanny, @takai, @hogelog, @l15n, @ko1, @mame, @hokaccha, @eisuke, @giga811, @riseshia, @inohiro, @ukstudio, @hfm, @davidstosik, @aadityataoaria, @sikachu, @asonas, @sorah, @pndcat, @sankichi92, @kojitaniguchi, @to9nariyui

登壇スケジュール

はじめに、社員が登壇するセッションのスケジュールを紹介します。

1日目 4月18日(木)

  • 14:20-15:00 笹田耕一(@ko1): Write a Ruby interpreter in Ruby for Ruby 3
    本発表では、RubyインタプリタをRubyで記述するために必要となる要素技術についてご紹介します。Rubyで、といっても、コア部分は相変わらずCで書く必要がありますが、組込メソッドをなるべくRubyで置き換えていきたいという話になります。ここで、課題になるのは、(1) Ruby だと呼び出しが遅いかもしれない (2) Ruby だと読み込みが遅いかも知れない、の二つです。本発表では、これらをどのように解決するかについて議論します。
  • 15:40-16:20 遠藤侑介(@mame): A Type-level Ruby Interpreter for Testing and Understanding
    Ruby 3の静的解析技術の1案として、Rubyプログラムを型レベルで仮想的に実行するRuby処理系を提案します。すでに提案されている他の静的解析と異なり、型注釈がなくてもなんとなく検査・推定ができるところが特徴です。仮想的な実行の過程で発見された型エラーの可能性や、メソッド呼び出しの引数や返り値の型を報告することで、ユーザのテストやプログラム理解を支援することを目指しています。本発表では、詳しいアイデアと、他手法との比較、現在どこまで実装できているかなどを説明します。

2日目 4月19日(金)

  • 17:20-18:20 ライトニングトーク
    • 井上寛之(@inohiro): Write ETL or ELT data processing jobs with bricolage.

3日目 4月20日(土)

  • 10:00-11:10
    • Cookpad Presents: Ruby Committers vs the World こちらのお時間では、Cookpad Ltd CTOの Miles Woodroffeがご挨拶いたします。また、笹田耕一と遠藤侑介が司会を務めます。
  • 11:20-12:00 Sangyong Sim(@riseshia): Cleaning up a huge ruby application
    cookpad.com を支える巨大なレポジトリから未使用コードの削除を進めています。この作業は比較的コストが高い割にリターンが見えづらいです。この問題をどういった仕組みで解決しようとしているのかについて、お話しします。 Ruby 2.6 で導入された Oneshot coverage などを利用し、本番で実行されたコードを記録する仕組みも紹介します。

ブース

RubyKaigi 2019にて出展するクックパッドブースでは、福岡県にゆかりがある料理の限定レシピ集をお配りするほか、 @mame@ko1が考案した Cookpad Daily Ruby Puzzles を一日3問ずつ公開いたします。各問題に最小数の文字を追加し、 "Hello World!"を出力してください。

# example
def foo
  "Hello world" if
    false
end

puts foo

早く回答できた方にはCookpad Pad RubyKaigi 2019 Edition など、数量限定で特別なプレゼントをご用意しています。みなさんの挑戦お待ちしております! また、下記スケジュールの通り、登壇者へのQ&Aタイムなどを予定しております。グッズの配布や昨年好評だった豆つかみもバージョンアップして行いますので、ぜひお立ち寄りくださいね。

f:id:tokunarigyozadaisuki:20190415112930j:plain

1日目 4月18日(木)

  • 15:10-15:40 【ブースイベント】午後休憩: Q&Aタイム by @ko1
    この時間は、クックパッドブースに @ko1がおりますので、1日目 14:20-15:00 Write a Ruby interpreter in Ruby for Ruby 3 に関するご質問がある方は、ぜひこの時間にブースにて、本人に聞いてみてください。

2日目 4月19日(金)

  • 12:30-13:00 【ブースイベント】ランチ休憩: Q&Aタイム by @mame
    この時間は、クックパッドブースに @mameがおりますので、1日目 15:40-16:20 A Type-level Ruby Interpreter for Testing and Understanding に関するご質問がある方は、ぜひこの時間にブースにて、本人に聞いてみてください。
  • 15:00-15:40 【ブースイベント】午後休憩: クックパッド子会社 ウミーベ株式会社タイム
    昨年、クックパッドグループに加わったウミーベ株式会社。ウミーベのオフィスはRubyKaigi 2019の開催地、福岡県福岡市の海辺にあります。本時間には、ウミーベCTO @Niaがブースにおりますので、ウミーベのサービスについてご質問がある方、福岡で働くことに興味がある方はぜひお気軽に話しを聞いてみてください。

3日目 4月20日(土)

  • 12:30-13:00 【ブースイベント】ランチ休憩:Q&Aタイム by @riseshia
    この時間は、クックパッドブースに @riseshiaがおりますので、3日目 11:20-12:00 Find out potential dead codes from diff に関するご質問がある方は、ぜひこの時間にブースにて、本人に聞いてみてください。

  • 15:10-15:40 【ブースイベント】午後休憩:Cookpad Daily Ruby Puzzlesの解説
    @mameより、ブースにて三日間に渡って出題した全9問の Cookpad Daily Ruby Puzzles の解説を行います。解けた方も解けなかった方も、考案者からの解説を聞いてスッキリしてください! 

おわりに

クックパッドでは、料理で世界に挑戦する仲間を探しています。クックパッドで働くことにご興味のある方は、お気軽にブースにお越しください。また、会場でクックパッド社員をお見かけの際には、お声がけいただけますと嬉しいです! みなさまにお会いできることを社員一同楽しみにしております。

RubyKaigi 2019 "A Type-level Ruby Interpreter for Testing and Understanding"の発表要旨

$
0
0

こんにちは、クックパッドで仕事で Ruby の開発をしている遠藤(@mametter)です。もうすぐ RubyKaigi ですね! クックパッドはいろんな形で RubyKaigi に参加していく予定なのでよろしくお願いします。詳しくは昨日の記事をごらんください。

さて、そういうわけで RubyKaigi です。遠藤は "A Type-level Ruby Interpreter for Testing and Understanding"という発表を予定しています。遠藤の発表予定の内容をあらかじめざっと紹介してみます。

この予稿は発表資料を作り終えてから書いているのですが、発表資料よりも要点がまとまっている気がします。

はじめに: Ruby 3の静的解析

2020 年にリリースが予定されている Ruby 3 は、「静的解析」「高速化」「並列性」の 3 つを備えることを目標に掲げています。この発表は 1 つめの「静的解析」に関わるものです。

Ruby 3 に向けた型システムとして、SteepSorbetが提案されていますが、いずれもメソッドの型はユーザが指定する前提になっています。

本発表では、「型は絶対に書きたくないでござる」の人たちのために、型注釈がない Ruby プログラムに適用可能な静的型解析器、「型プロファイラ」を提案します。

型プロファイラとは

型プロファイラは、

  • 型注釈がない素の Ruby プログラムを入力して、
  • 型エラーの可能性を警告したり(Testing)、
  • 型シグネチャのプロトタイプを生成したり(Understanding)

できるツールです。

Testing の例

型エラーを警告する例を示します。

deffoo(n)
  if n < 10
    n.timees {|x| # TYPO!
    }
  endend

foo(42)

このプログラムは Integer#timesを typo して timeesと書いています。このプログラムに型プロファイラを適用すると、次のような出力が得られます。

$ ./run.sh /tmp/test.rb
/tmp/test.rb:3: [error] undefined method: Integer#timees
Object#foo :: (Integer) -> (NilClass | any)

Integer#timeesは undefined である、というエラーが出ています。なお、元のプログラムを普通に実行するだけではこのバグを検知できないことに注意してください(n < 10 なので)。

Understanding の例

次は型シグネチャのプロトタイプを得る例です。

deffoo(n)
  n.to_s
end

foo(42)
foo("STR")
foo(:sym)

このプログラムに型プロファイラを適用すると、次のような出力が得られます。

$ ./run.sh /tmp/test.rb
Object#foo :: (Integer) -> String
Object#foo :: (String) -> String
Object#foo :: (Symbol) -> String

fooはオーバーロードされていて、

  • Integer を受け取ったら String を返す
  • String を受け取っても String を返す
  • Symbol を受け取っても String を返す

ということを表現する型シグネチャのプロトタイプとして使えます。

これが型プロファイラの基本です。

もっとデモがみたい!

このへんにいろいろ転がってます。詳しくは発表で。

https://github.com/mame/ruby-type-profiler/tree/master/smoke

型プロファイラをどのように使うか?

おおまかに 2 つの使い方を想定しています。

  1. 開発中にテストと合わせて実行し、型エラーの可能性を調べてみる
  2. Ruby プログラムから型シグネチャをプロトタイプし、手修正の上で型検査器(Steep や Sorbet)を使ってきちんと検証する

前者の使い方は、従来の Ruby のプログラミング体験にあまり影響を与えず、静的解析を補助的なテストとして利用する方法です。 推定される型シグネチャは特に利用しないか、参考程度にします。

後者の使い方は、型シグネチャの生成支援です。 型シグネチャをあとからまとめて書きたい場合、特に既存の Ruby プログラムに対して型検査器を適用する際に役立つと思います。 また、よくわからない Ruby プロジェクトをいじらないと行けないとき、プログラムの中にどのようなクラス・メソッド定義があるかを俯瞰するためにも有用かもしれません。

型プロファイラのメリット・デメリットは?

メリットはただ 1 点に集約されます。

  • 型注釈がなくても型検査・型推論っぽいことができる

デメリットはいろいろあります。

  • 誤った警告(false positive)を出すことがある
  • 各メソッドを起動する型レベルのテストが必要、足りないとバグの見逃しにつながる
  • 原理的に扱えない Ruby の言語機能がある(たとえば Object#send や特異クラス)
  • スケーラビリティに問題がある

長くなるのでこの記事では説明を省きますが、発表ではこれらの問題の分析や、それらに対して何ができると考えているかについて駆け足で語ります。

型プロファイラはどのように動いているか?

今回のメインコンテンツです。型レベルで Ruby プログラムを解釈実行するインタプリタがコアになっています。

deffoo(n)
  n.to_s
end

foo(42)

というコードがあったとき、普通のインタプリタであれば

  1. 関数 fooに整数 42 を渡して呼び出す
  2. 関数 fooの中を n = 42の環境で評価する
  3. n.to_sを実行した結果の文字列 "42"を関数 fooがリターンする
  4. foo(42)の呼び出しが "42"を返して実行再開する

というように実行が進んで行きます。型プロファイラはこれを型レベルで行います。つまり、

  1. 関数 fooに整数 Integer を渡して呼び出す
  2. 関数 fooの中を n :: Integerの環境で評価する
  3. n.to_sを実行した結果の String を関数 fooがリターンする
  4. foo(42)の呼び出しが String を返して実行再開する

このような型レベルでの実行を記録し、各関数に渡された引数(Integer)や返した返り値(String)を集めて、型シグネチャのような形式にして出力します。

分岐があったらどうするか?

関数に型レベルの情報しか渡さないので、分岐の条件を正確に評価できなくなります。たとえば次の例。

def foo(n)
  if n < 10
    n
  else
    "string"
  end
end

foo(42)

n < 10という条件式がありますが、nには 42 という具体的な値ではなく Integer という型レベルの情報しか入っていないので、分岐を正確に実行することはできません。

型プロファイラは、分岐があったら状態をフォークします。つまり、true の可能性と false の可能性を両方とも実行します。上の例で言うと、true のケースは n (Integer) をリターンする、false のケースは "string" (String) をリターンする、ということで、これらを組み合わせて

$ ./run.sh /tmp/test.rb
Object#foo :: (Integer) -> (String | Integer)

というシグネチャを生成して出力します。

このフォークのせいで、うまくないコードを書くと状態爆発につながってしまいます。通常のコードで状態爆発が起きにくいように抽象化の粒度や状態管理をうまくやるのが、型プロファイラの設計のむずかしいところです。

この手法は何なのか?

普通の型システムとはいろいろ異なると思います。普通の型システムは、メソッドなどのモジュール単位で検査できるよう、メソッド間をまたがない(intra-procedural な)解析になるように設計されます。この点、型プロファイラはメソッド呼び出しがあったときにメソッド本体を呼び出すので、メソッドをまたがる(inter-procedural な)解析になっています。

型プロファイラの手法を指すぴったりの技術名は調べてもわかりませんでしたが、どちらかというと、抽象解釈や記号実行といった技術に近いようです。

なお、inter-procedural な解析は、先に問題として述べたとおり、スケーラビリティとの戦いになりやすく、型プロファイラも例外ではありません。発表ではどのように対策してきたか、対策していきたいと考えているかを議論します。

型プロファイラの完成度は?

発表で詳しくいいますが、端的に言えば、残念ながらまだまだ完成度が低いです。ソースコードは mame/ruby-type-profilerに公開してありますが、正直に言って、まだまだみなさんのコードに適用を試せる段階にはないです。スケーラビリティのための根本的な対策検討から、地道な組み込み機能のサポートまで、やることがたくさんあって手が回っていません。型を書かない Ruby 体験を維持したいと思っている方は協力をご検討いただけると嬉しいです。

まとめ

本発表では、型注釈がない Ruby プログラムに適用可能な静的型解析器、「型プロファイラ」を提案します。 抽象解釈の考え方に基づいていて、現在のところ型なし Ruby 体験を維持できそうな静的解析アプローチの唯一の提案になっています。

発表では、ここまでに実装できている機能のデモや、現状の問題点の解説、preliminary ながら評価実験などをいろいろご紹介したいと思ってます。 ぜひ聞いていただいて、前向きに興味を持ってくれた方とも批判的な立場の方ともいろいろ議論できることを楽しみにしています。

RubyKaigi 2019: Write a Ruby interpreter in Ruby for Ruby 3

$
0
0

技術部の笹田です。フルタイム Ruby コミッタとして働いているので、明日から始まる RubyKaigi 2019 は仕事で行きます。あまり日のあたることが少ない我々の晴れの舞台です。

宣伝もかねて、RubyKaigi 中に自分がどんな仕事があるか並べてみました(クックパッド全般の話は、「クックパッド一同は、RubyKaigi 2019でみなさんにお会いできることを楽しみにしています!」 をご覧下さい)。

  • 毎朝、クックパッドブースで「Cookpad Daily Ruby Puzzles」を紙で配付しますので、興味がある方はお持ち下さい。
  • 1日目
    • 11:20-「Ruby 3 Progress Report」まつもとさんの keynote 後、Ruby 3 の進捗みたいなことをご紹介します。
    • 14:20-「Write a Ruby interpreter in Ruby for Ruby 3」私の accept された発表です。
    • 15:00- 休憩時間、クックパッドのブースにおりますので、ご質問がある方はお運びいただければ幸いです。
  • 3日目
    • 10:00- 「Ruby Committers vs the World」恒例のコミッタを壇上に並べる出し物です。Q&A になるかと思います。https://forms.gle/f7zZt1pKCA5HTABe9から、まつもとさんやコミッタに質問をお寄せ下さい。
    • 15:00- 休憩時間、クックパッドのブースにて、「Cookpad Daily Ruby Puzzles」の解説が遠藤さんからあるのを眺める予定です。
    • 終了後、RubyKaigi 子供会という、子供連れが集まる宴会を企画しています(保護者会だったかもしれない)。

あれ意外と少ない。京都でやったときは、一日中並列化の議論をしていた気がする。開催の前日に Developer's meeting と、翌日に after hackathon があるので、まぁやはり大変かも知れません。

さて、本稿では、私の発表、「Write a Ruby interpreter in Ruby for Ruby 3」についてご紹介します。下手な英語で発表する予定なので、こちらでは日本語で記事として残しとこうという意図になっています。

この発表は?

f:id:koichi-sasada:20190417011701p:plain
title_image

発表タイトルを直訳すると、「Ruby 3 にむけて、Ruby でインタプリタを書いていこうぜ」という感じになるでしょうか。

今、MRI (Matz Ruby Interpreter) は、ほぼすべて C で書かれています。タイトルを読むと、これを Ruby に全部置き換えよう、と見えるかも知れませんが、意図としては、「Ruby で書いた方がよいところは Ruby で書けるようにしよう」というものです。Ruby で Ruby をすべて self-host しよう、みたいな RubyでつくるRubyのような話ではありません。

現実的に、良い感じの仕組みを導入して、Ruby 3 をよりよくしましょう、という提案になります。

発表資料は http://www.atdot.net/~ko1/activities/2019_rubykaigi2019.pdfからダウンロード頂けます(修正等、随時更新が入ります)。

背景:現状と問題点

MRI での組込クラス・メソッドの定義の方法

現在、Ruby の組込クラス・メソッドのほとんどは、C で記述されています。String だとこんな感じ。

void
Init_String(void)
{
    rb_cString  = rb_define_class("String", rb_cObject);
    ...
    rb_define_method(rb_cString, "<=>", rb_str_cmp_m, 1);
    rb_define_method(rb_cString, "==", rb_str_equal, 1);
    rb_define_method(rb_cString, "===", rb_str_equal, 1);
    rb_define_method(rb_cString, "eql?", rb_str_eql, 1);
    ...
    rb_define_method(rb_cString, "length", rb_str_length, 0);
    rb_define_method(rb_cString, "size", rb_str_length, 0);
    ...
}

基本的に、rb_define_classというクラスでクラスを定義して、そのクラスに rb_define_methodでメソッドを追加していく、というものです。rb_define_methodでは、名前と実装している関数、それから arity (引数の数)を指定します。String#lengthの場合は、rb_str_lengthという関数で実装されているようです。

VALUE
rb_str_length(VALUE str)
{
    return LONG2NUM(str_strlen(str, NULL));
}

こんな感じで、C で Ruby のメソッドが記述できます。この場合、String#lengthメソッドが呼ばれると、最終的には rb_str_length()が呼ばれる、というものです。C プログラマなら、見ればわかるような構造になっていて、わかりやすいです。

(実は、prelude.rbという、Ruby で定義を書く方法もあったりしますが、あまり使われていません)

なお、このように C で定義されたメソッドを C メソッド、Ruby で定義されたメソッドを Ruby メソッドと呼ぶことにしましょう。

現状の問題点

さて、このわかりやすい構造ですが、現在はいくつか問題があります。4つにまとめてみました。

  • (1) アノテーション(メタデータ)の問題
  • (2) 性能の問題
  • (3) 生産性の問題
  • (4) API に context を追加したい問題

これらの問題を解説します。

(1) アノテーション(メタデータ)の問題

C メソッドには、いくつかの意味で情報が足りていません。

(a) Ruby メソッドに比べて情報が足りません。

例えば、Method#parametersという、パラメータ名を取得刷るメソッドを利用すると、

defhello(msg) puts "Hello #{msg}"; end
p method(:hello).parameters
#=> [[:req, :msg]]

このように、Ruby メソッドの引数の名前 msgを取得することができます。他にも、バックトレース情報など、こんな感じで Ruby メソッドに比べて情報が落ちているところがあります(時々聞く、stack-prof で C メソッドが出てこなくて困る、というのは、これが理由です)。

これらは、Ruby で定義すれば、持っていたはずの情報になります。

(b) 最適化のために必要な情報が足りません。

とくに、メソッドをまたぐ最適化を行おうとすると、あるメソッドがどのような性質を持つか、例えば「副作用を持つ・持たない」という情報はとても重要になります。しかし、C で実装されたメソッドの性質を調べようとすれば、C のソースコードの解析が必要になり、現実的ではありません。

たとば、str.gsub("goodby", "hello")というプログラムでは gsubに渡した引数を弄るかも知れないので、呼び出す度に2つの文字列を生成します。しかし、gsubは引数を弄らないので、本来であれば、frozen な文字列を(毎回生成せずに)渡すだけで良いはずです。frozen-string-literal pragma を使えば、プログラマがそのように指定することができますが、煩雑です。gsubがこのようなメソッドである、という情報を付加できれば、MRI が自動的に判断できそうです(がんばれば)。

これらは、MRI 開発者ががんばって付けていく情報になります。

(c) どれくらいメソッドが定義されるか、事前にわかりません。

rb_define_methodで定義すると、起動が終わらないと、あるクラスに、どれくらいのメソッドが定義されるかわかりません。わかっていれば、先にメソッドテーブルをそのサイズで確保する、みたいなことができます。が、現在そういうのができません。

定義が事前に解析出来る形で書いてあれば、得られる情報です。

(2) 性能の問題

多くの場面で、C は Ruby よりも速いです。いろんな理由がありますが(最近、なぜrubyは他の言語と比べて遅いのでしょうか?という Quora の質問にこたえてみましたので、よかったら参考にして下さい)、まぁ適材適所、向いてる言語を使うべきでしょう。Ruby の主要部分を C で書くのは、そこそこ妥当だと思います(現代では、Rust などのより安全な言語を視野にいれるべきだとは思います)。

ですが、いくつかの場面で、実は Ruby は C で書くよりも速いことがあります。典型的な例は、キーワード引数の処理です。

# Rubydefdummy_func_kw(k1: 1, k2: 2)
  dummy_func2(k1, k2)
end

こういう処理を C で書こうとすると、結構面倒ですがこんな感じになります。

static VALUE
tdummy_func_kw(int argc, VALUE *argv, VALUE self)
{
    VALUE h;
    ID ids[2] = {rb_intern("k1"), rb_intern("k2")};
    VALUE vals[2];

    rb_scan_args(argc, argv, "0:", &h);
    rb_get_kwargs(h, ids, 0, 2, vals);
    return tdummy_func2(self,
                        vals[0] == Qundef ? INT2FIX(1) : vals[0],
                        vals[1] == Qundef ? INT2FIX(2) : vals[1]);
}

これらのメソッドの速度を比較してみましょう。

f:id:koichi-sasada:20190417011812p:plain
キーワード引数のあるメソッドの呼び出しの速度比較

キーワード引数がないときは、C の方が速いです。というのも、Ruby での dummy_func2()呼び出しは、C での tdummy_func2)()関数呼び出しよりも圧倒的に遅いからです。

しかし、キーワードを与えると、圧倒的に Ruby で書いた方が速いです。というのも、キーワード引数のあるメソッドに、Ruby でキーワード引数を渡すときは、ハッシュオブジェクトを生成しない、特別な最適化が施されているからです。

例外処理も、同じような理由で Ruby で書いた方が速いです。

# in Rubydefem_dummy_func_rescuenilrescuenilend
static VALUE
dummy_body(VALUE self)
{
    return Qnil;
}
static VALUE
dummy_rescue(VALUE self)
{
    return Qnil;
}
static VALUE
tdummy_func_rescue(VALUE self)
{
    return rb_rescue(dummy_body, self,
             dummy_rescue, self);
}

f:id:koichi-sasada:20190417011909p:plain
例外処理の速度比較

このように、たまにある「Ruby で書いた方がいい場合も、C で書いちゃう」という問題があります。

(3) 生産性の問題

(2) で例を出したように、Ruby だと数行のものが、C で書くと何十行、複数関数にまたがる、みたいなことがよく起きます。 C で表現するためにしょうがない部分なんですが、大変です。

例外処理やイテレータ、キーワード引数の処理なんかが該当しそうです。

また、あまり呼ばれないメソッドの場合、ささっと Ruby で定義しちゃってもいいかもしれませんね。今だと gem でやれって言われるかもしれませんが...。

余談ですが、私は C でキーワード引数の処理を書きたくなさ過ぎて、prelude.rbで Ruby 2.6 で導入した TracePoint#enable(target:)を実装しました。楽だったー。

(4) API に context を追加したい問題

rb_deifne_method()で登録する関数の引数は、基本的に selfとパラメータ情報になります。しかし、我々が進めている並列処理機構である Guild では、現在の「コンテキスト」情報を渡す必要があります。mruby における mrb_state *です。

# mruby String#length
static mrb_value
mrb_str_size(mrb_state *mrb, mrb_value self)
{
  mrb_int len = RSTRING_CHAR_LEN(self);
  return mrb_fixnum_value(len);
}

Thread-local-storage (TLS) に保存する、と言う方法もありますが、とくに shared library 経由で利用すると、とても遅いことが知られています(詳細は、笹田等:Ruby 用マルチ仮想マシンによる並列処理の実現 (2012))。そこで、第一引数に、mruby みたいに情報を引き渡したすために API の変更が必要です。

問題の最後 (4) に持ってきましたが、個人的にはこれが一番なんとかしたい問題です。ただ単に API を変更しても、なかなか追従してもらえないんですが、いろんな特典がついたほうが移行しやすいよね、という戦略です。

問題のまとめ

4つの問題をまとめました。

  • (1) アノテーション(メタデータ)の問題 -> DSL が必要
  • (2) 性能の問題 ->時々 Ruby のほうが速い
  • (3) 生産性の問題 -> Ruby で十分のときがある
  • (4) API に context を追加したい問題

(1) は、新たにメソッド定義のための DSL があれば解決しそうです。あれ、そういえば、DSL を構築しやすい言語に心当たりがあったような?

解決案:Ruby をつかおう!

問題を解決するために、Ruby で定義もしくは宣言を行うことを考えました。すべてを Ruby で置き換えるわけではなく、C で書いた方がよいところは C で書いて、Ruby の定義から簡単に呼び出せるようにすれば(FFIの導入)、既存の資産も有効活用でき、C の圧倒的な性能も利用できて良さそうです。

Ruby で書いておけば、後から解析することで、いろいろなことがわかります。また、内部DSL的にメソッドにアノテーションを付けることも可能でしょう。

問題点はこのように解決できます。

  • (1) アノテーション(メタデータ)の問題 -> Ruby で DSL を書いて解決
  • (2) 性能の問題 ->素直に Ruby が得意なところで Ruby を書けば解決
  • (3) 生産性の問題 -> Ruby で簡単に済むところは Ruby で済ますことで解決
  • (4) API に context を追加したい問題 -> FFI で context を渡すようにすれば解決

新しい書き方

では、具体的にどんなふうに書いていくでしょうか。

文字列のメソッドを定義する string.rbを新設し、lengthメソッドを定義することを考えます。

# string.rbclassStringdeflength
    __ATTR__.pure
    __C__.str_length    
  endend
# String#length impl. with new FFI
static VALUE
str_length(rb_ec_t *ec, VALUE str)
{
    return LONG2NUM(
      str_strlen(str, NULL));
}

こんな感じで、__C__.str_lengthと書くと、str_length()が呼ばれる、という仕組みです。

なお、__C__は適当です。多分、変わると思います。また、特別な実行モードでのみ利用可能になると思います。普段はローカル変数(もしくはメソッド名)ですね。

__ATTR__.pureも適当にでっちあげてるだけですが、こんな感じで、String#lengthの属性を人間が書けるようにしていければなと思っています。

これを使うと、プログラマはこんな感じになると思います。

  • Ruby の機能を使うことで、簡単に書けるところは簡単に書けるようになる。
  • C の関数を簡単に呼べるので、性能を落とさずにちゃんと書けるようになる。
  • いくつかの点に気を付けなければならない
    • GVL リリースや、GC タイミングなどが変わるので、気にする人はきにしないといけません。
    • 従来通りにしたければ、単に C の関数を呼び出す、というようになります。

疑問

さて、どうでしょうか。書きやすく、良さそうな感じがしないでしょうか。

ただ、きっと、パフォーマンスについて気にする人(私とか)は、次の点が気にならないでしょうか。

  • ランタイムオーバヘッド:FFI で C 関数呼び出しって遅いんじゃないの?
  • スタートアップ時間:Ruby スクリプトを読み込むから、スタートアップ時間が長くなってしまうんじゃないの?

この二つの疑問に答えるために、本発表では、次の二つの技術的成果についてご紹介します。

  • 高速な FFI を実現するための VM 命令の追加
  • ロード時間削減のためのコンパイルバイナリフォーマットの改善

ざっくり結論を申しますと、この二つの技術的成果を用いることで、C で全部書くよりは、若干遅いけど、でも十分速くなるので、多分問題ないんじゃないかな? という感じです。

ここまできて、やっと本題にたどり着きました。

高速な FFI を実現するための VM 命令の追加

長くなったので、手短に行きます。

__C__.func(a, b)のように関数を呼び出せるようにするために、invokecfuncという命令を VM に追加しました。fiddle などのミドルウェアを用いずに C の関数を呼び出すので高速です。

# string.rbclassStringdeflength
    __C__.str_length    
  endend

こういうプログラムは、

== disasm: #<ISeq:length@string.rb:10>
0000 invokecfunc                     
0002 leave

こんな感じでコンパイルされます。

ただ、invokecfuncを用いる関数呼び出しは、従来の C メソッドよりもオーバヘッドがあります。

  • (1) 引数を VM スタックに push するので遅い
  • (2) leave 命令でフレームを抜けるので、1命令実行が余分にかかり遅い

そこで、(1) の問題のために、__C__.func(a, b)に渡す実引数が、そのメソッドの仮引数 def foo(a, b)とまったく等しいとき、VM スタックにプッシュするのではなく、関数の引数にメソッドの引数をそのまま利用する invokecfuncwparam命令を追加することにしました。

defdummy_func2 a, b
  __C__.dummy_func2(a, b)
end
0000 invokecfuncwparam<dummy_func2/2>
0002 leave

これで、「(1) 引数を VM スタックに push するので遅い」の問題が解決します。組み込み関数は、だいたい C で書いてある関数をそのまま呼ぶことになるんじゃないかと思うので(つまり、C 関数への delegator のような実装になるんじゃないかと思うので)、この命令を作る価値はあるのではないかと判断しました。

そして、leaveをわざわざ次命令でやるのは無駄じゃないかと言うことで、invokecfuncwparam命令の次の命令が leaveの場合、その命令内でフレームを終了させる invokecfuncwparamandleave命令を用意しました。

つまり、上記 dummy_func2関数は、次のようにコンパイルされます。

0000 invokecfuncwparamandleave …
0002 leave

TracePointreturnイベントに対応するために、leaveイベントは残す必要がありますが、基本的には invokecfuncwparamandleave命令のみ実行するメソッドになります。

評価

さて、結果はどうなったでしょうか。

defdummy_func0
  __C__.dummy_func0
enddefdummy_func1 a
  __C__.dummy_func1(a)
enddefdummy_func2 a, b
  __C__.dummy_func2(a, b)
end

このように定義したメソッドと、これに対応する C メソッドの実装の実行時間を比べてみたのが次のグラフです。

f:id:koichi-sasada:20190417011942p:plain
FFIの高速化の評価結果

invokecfuncを用いるのみが baseline ですが、それだと C メソッドよりも遅かったのが、最適化を組み合わせることで、Cメソッドよりも高速に実行できることがわかります。

発表資料には、もう少しいろいろな評価があるので、そちらもご参照下さい。

まとめと今後の課題

まとめると、「FFI を用いると、ランタイムオーバヘッドは高いのでは?」という疑念に対し、「なんでもやる強い気持ちをもって最適化を行うと、問題ない(ことが多い)よ」ということです。性能を気にせず、Ruby で書けそうです。

今後の課題として、オプショナル引数などはまだ遅いので、オーバーローディングの仕組みを入れるなどして、典型的な例は速い、みたいなことを目指せればと思っています。引数の数によってメソッド実装を選ぶようなことを想定していますが、インラインキャッシュが使えるので、そこそこ feasible なのではないかと思っています。

関連研究に、私が10年前にやっていた「Ricsin: RubyにCを埋め込むシステム (2009.3)」という研究があります。これは、Ruby の中に、直接 C のプログラム片を埋め込めるようにする、という研究です。

# Writing C in Ruby codedefopen_fd(path)
  fd = __C__(%q{ // passing string literals to __C__ methods    /* C implementation */    return INT2FIX(open(RSTRING_PTR(path), O_RDONLY));})
  raise'open error'if fd == -1yield fd
ensureraise'close error'if-1 == __C__(%q{    /* C implmentation */    return INT2FIX(close(FIX2INT(fd)));})
end

C の中から、Ruby の変数にアクセスできるのがキモ面白いところだと思っています。将来的には、こういう拡張ができるようにしても面白いかも知れないと思っています。

なお、本稿では、FFI の実装に必要になる関数テーブルの作成部分は、ちょっと面倒なので省略しました。正直、ここがブートストラップで一番難しいところなんですよね。

ロード時間削減のためのコンパイルバイナリフォーマットの改善

ランタイムオーバヘッドの懸念が解消されたら、次はスタートアップタイムが伸びてしまうんじゃないかという懸念についての返答です。Ruby でメソッドを定義するようにしたら、複数の .rb ファイルを起動時に読むから遅そうなんじゃないの、という話です。

Ruby では 2.3 から、バイトコード(MRI では ISeq という用語を使います)をバイナリにダンプする仕組みを持っています。

# dump
bin = RubyVM::InstructionSequence#to_binary# loadRubyVM::InstructionSequence.load_from_binary(bin)

AOT コンパイルみたいな用語を使ってもいいと思います。bootsnap でも使っていますね。事前にコンパイルすることで、コンパイルのコストを抑えられるんじゃないか、という期待で作ったものです。

で、そのバイナリデータを、例えば C の配列表現にして MRI と一緒にコンパイルすれば、MRI のバイナリに統合することができます。ちなみに、起動後に mmap しても、だいたい同じような感じにすることができます。実験では、前者を使いましたが、正直最初から mmap でやればよかったな。

で、それだけだとなんなので、二つの仕組みをさらに有効にすることで、より効率的に出来るんじゃないかと思います。

Lazy ローディング

ISeq は、ツリー構造になっています。トップレベル iseq が、クラス定義 iseq をもち、それがメソッド iseq を持つ、という感じです。メソッドを起動されない限り、メソッド iseq は使われません。つまり、iseq のロードを、実際に使われるまで、遅延することができるということです。これを lazy ローディングと言います。

実は、この lazy ローディング、Ruby 2.3 の段階で入っていたんですが(vm_core.h の USE_LAZY_LOADマクロ)、イマイチ使わないかなーと思ってたんですが、起動時に全部の iseq を作るよりも、実際に使うメソッドやブロックだけロードするほうが圧倒的に速いので、これを有効にしちゃおうかなあと思っています。

たいてい、あるプログラムで呼ばれるメソッドなんて、定義されたメソッドのごく一部でしょうから、そこそこ納得感ある話なんじゃないかと思います。

ロード済みかイチイチチェックが入るので、若干遅くなるんですが、分岐予測で十分カバー出来る範囲かな、と思っています。

なお、この仕組みについては、RubyKaigi 2015 の私の発表や、「笹田等: Ruby処理系のコンパイル済みコードの設計 (2015)」に詳しいです。もう3~4年前なんだな。

複数のファイルをサポート

現在の compiled binary は、一つのファイルが一つのバイナリを出すようにしかなっていません。しかし、複数のファイルをまとめて一つのバイナリにすれば、共有部分が増えて、リソースが若干節約できます(多分)。

そこで、複数ファイルを一つの compiled binary にまとめることができるようにしました。現在は数値インデックスでしかアクセス出来ませんが、ファイル名でアクセスできるように拡張する予定です。

bin = RubyVM::InstructionSequence.to_binary(iseq1, iseq2, ...)と複数の iseq を読み込み、

loader = RubyVM::InstructionSequence::Loader.new(bin)
iseq0 = loader.laod(0)

のように取り出すことができるようにしてみました。

評価

評価のために、3000個のクラス C0~C2999 を作り、各クラスが 1~20個のメソッドを持つ(def m0; m; endのような単純なメソッド。全合計3万メソッドくらい)、というサンプルを作って実験してみました。

  • 1ファイルに詰め込む場合
    • .rb が 582KB
    • compiled binary が 16MB
    • それを C の配列表現にすると 79MB(!)
  • 各クラスごとにファイルを作る
    • .rb が 3000 個
    • まとめた compiled binary が 17MB
    • それを C の配列表現にすると 86MB
  • 従来の C メソッドでの定義の仕方を用いると、4.2MB の .c

この3通りを用いて、ロードして Ruby が起動する時間をはかってみました。なお、--disable-gemsで rubygems などのライブラリはロードしないようになっています。

結果は次のようになりました。

f:id:koichi-sasada:20190417012010p:plain
ロード時間の評価結果

結果を見ると、従来の C での定義が最も速く 27.5 秒で、lazy loading を用いることで、だいたい 2 倍程度の性能低下で済む、という具合です。

単なる compiled binary のロードだと 3 倍遅い。普通に .rb としてロードするよりも6~16倍程度遅い、という結果になりました(一番下の結果ひどいな)。というわけで、従来手法に比べると、やはり速いのだけれど、まだ C メソッド定義に及ばず、というところです。

まとめと今後の課題

スタートアップタイムについては、従来の C メソッドのロード時間より遅い、ということはなかったのですが、まだ若干遅いです。

.rb を書いておくと、事前にどのクラスにどんなメソッドが定義される、というのがわかるので、先にテーブルだけ作っておいて、メソッド問い合わせが来たときに初めて iseq のロードを始めるような、より lazy なやり方なんかが効くんじゃ無いかと思います。そこまでやれば、C メソッドのロードよりも速くなるんじゃないかな?

あと、単純にコンパイル済みバイナリがむっちゃでかいんですよね。わざと小さくしないようにしたんですが、さすがに大きすぎなのでなんとかしたい。多分、簡単に 1/5 くらいにはなると思います。誰かやってくれません?

本稿のまとめ

本稿では、私の RubyKaigi 2019 の発表である「Write a Ruby interpreter in Ruby for Ruby 3」について述べました。

現在 MRI では、ほぼすべて C で記述されていますが、それを良い感じに Ruby と混ぜるために、特別な FFI 記法の導入を提案しました。

そして、そこで懸念される「ランタイムオーバヘッド」および「スタートアップタイムの増加」について、いくつかのテクニックをご紹介し、そこそこ feasible な結果を出すことで、懸念をそこそこ払拭できたんじゃないかと思います。

現在の組込クラス・メソッドの定義を書き換えるとなると、多くの人手が必要になります。まだ、この方針で Ruby 3 向け(Ruby 2.7 向けかな?)に書き換えるという合意は取れていませんが、取れたらばばばーと書き換える作業が発生します。ある程度機械的な作業になるんですが、良い機会なので興味がある方、一緒にやりませんか?

われわれは Ruby Hack Challenge というイベントを開催しており、次回は Ruby Hack Challenge Holiday #3が 5/11 (土) に行います。こういう場で、Ruby (MRI) 開発に参加してくれる方がいらっしゃいましたら、お声かけ頂けましたら幸いです。

というわけで、RubyKaigi 2019 でお会いできることを楽しみにしております。

Bitrise & Cookpad Developer Meetupを開催しました

$
0
0

モバイル基盤部の@hiragramです。先日try! Swiftに合わせて来日したBitriseチームを恵比寿オフィスに招いてミートアップを開催しました。

Bitriseはモバイルアプリ開発に特化したクラウドCIサービスで、最近日本での採用もスタートされたそうです。

cookpad.connpass.com

東京へのカウントダウン! - Bitrise Blog

ミートアップでは、モバイルCIをテーマにBitrise社のサービス紹介やクックパッド社内のモバイルアプリ向けCI環境やその上で行なっている取り組みについてのトークがありました。

まず、BitriseCTOのViktorさんから、Bitriseのサービス紹介とジョブの最適化についての発表をしていただきました。

モバイル基盤部長の@slightairが、社内のモバイルCI環境で動いているタスクや、社内向けベータ配信の仕組みについて発表しました。

クックパッドのモバイル向けCI環境では、Pull Requestごとに実行されるユニットテストの他に、日次で実行されるUIテストや、社内向けにベータ版を配信するサービスが動いています。品質を担保するために必要な各種テストやベータ版の配布を自動化することで、開発者は繰り返し発生するそれらの作業に追われる事無く、手元の開発に集中することができます。

また、毎週金曜日に実行されるAppStore Connectへの自動サブミットによって「機械に人間が合わせる」というリリースフローが確立されており、部署をまたいだスケジュールの調整や、コードフリーズ日のすり合わせなどをする必要が無くなりました。

過去のテックブログや前回のiOSDCなどでも社内向け配信やサブミットの自動化などについて紹介しているので、ぜひそちらも合わせて御覧ください。

クックパッドアプリはみんなが寝ている間にサブミットされる | クックパッド開発者ブログ

続いて、モバイル基盤部の@vinsentisambartが、モバイル向けCI環境の具体的な構成などについて発表しました。

クックパッドのモバイル向けCI環境は、Jenkinsの上に構築されており、 iOSは4台のMac mini、Androidは3組の EC2 Linux instance + Genymotion Cloud Instance という Slave 構成になっています。iOS/AndroidそれぞれのCIを実現するに当たって、それらの環境の良いところ/悪いところを紹介しています。周辺ツールの更新などが自由度高く行える一方で、マシンの追加や環境構築のコストなどが課題として指摘されています。

おわりに

クックパッドモバイル基盤部では、アプリ開発のフローを効率化/自動化することで、サービス開発者の生産性を高めるための取り組みをしています。開発者の生産性向上に興味がある方はぜひ一度クックパッドオフィスに遊びに来てください。

クックパッドメンバーに直接カジュアルにお声がけいただいてもいただいてもいいですし、以下のページからご連絡いただいても大丈夫です!お待ちしております!

クックパッド株式会社 採用サイト

Viewing all 726 articles
Browse latest View live