TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

Webフロントエンドエンジニアの権守です。 今回は、iQONのWebアプリのAPIリクエスト部分の仕組みを改善したことについて紹介します。 前提 このブログでも何度か紹介していますが、iQONでは、ネイティブアプリとWebアプリの両方で、共通のAPIを利用して開発を行っています。 そのため、通常のRailsアプリケーションと異なり、iQONのWebアプリ版のモデル部分では、DBへのアクセスを行わずAPIへのアクセスを行い、データを取得します。 こういった形式を扱うGemとしては her などがありますが、iQONでは、完全にREST形式でない、並列でリクエストを行いたいなどの理由から自前で実装しています。 問題 しかし、このモデル部分には次の二つの問題がありました。 APIリクエストの依存関係を記述できないため、実行タイミングを制御する必要がある APIリクエストのリクエスト処理とデータの取得処理を同時に記述できない 一つ目はパフォーマンスに、二つ目は可読性に問題があります。 擬似コードを例に問題について説明します。 旧リクエスト方式の擬似コード Item .find(params[ :id ], :item ) Item .recommend_items(params[ :id ], :recommend_items ) IqonModel .execute @item = get_result( :item ) # ... @itemを使う処理 ... @recommend_items = get_result( :recommend_items ) Item .search({ brand_id : @item [ :brand_id ]}, :brand_items ) # itemのリクエストに依存 IqonModel .execute @brand_items = get_result( :brand_items ) 上のような記述を行った場合、APIリクエストは次の画像に示すタイムラインのようになりえます。 この場合、依存関係を適切に考慮すると、次のようなタイムラインにでき、パフォーマンスは向上します。 この問題は偏に、実行タイミングをコードで制御していることが原因です。 理想的には、依存関係を考慮しつつ、並列度を最大化すべきです。 また、上の擬似コードでは大して気になりませんが、利用するAPIの量が増えるほど、各APIリクエスト処理と結果の取得処理などのコードの分離による可読性の劣化は著しくなります。 結果 上で述べた問題を解決し、次のように記述できるようにしました。 Item .find(params[ :id ], :item ) do | find_results | @item = find_results.first # ... @itemを使う処理 ... Item .search({ brand_id : @item [ :brand_id ]}, :brand_items ) do | results | @brand_items = results end end Item .recommend_items(params[ :id ], :recommend_items ) do | results | @recommend_items = results end IqonModel .wait_all_requests # 全スレッドの処理完了を待つ また、実際に問題を解決したことで、ページによってはサーバーサイドの処理時間が40msほど短縮されました。 実装について コールバック APIリクエストの各メソッドにコールバックを設定できるようにしたことによって、リクエスト処理と結果取得処理が分離しなくなっただけでなく、APIリクエストの依存関係を明示的に記述できるようになりました。 各APIリクエストのメソッドに渡されたブロックがコールバックに相当します。 スレッド 旧リクエスト方式では、IqonModel.executeが実行されたタイミングで、それまでに呼び出されたリクエストをまとめて、並列にリクエストするというものでした。 しかし、今回の改善では、明示的なexecuteを利用しない代わりに、各リクエスト時にスレッドを生成するようにしました。これによって、並列にリクエストしつつ、executeのタイミングを制御する必要がなくなりました。 一方で、スレッドの完了を制御する必要があります。 これについては、IqonModel.wait_all_requestsメソッドでリクエストによって生成されるスレッドに対してjoin処理を実行することで制御しています。 APIリクエスト部分を簡単化したコード class Item def self . find (id, label, &block) IqonModel .request(label, " /item/ #{ id }" , block : block) end end class IqonModel def self . init @request_threads = [] end def self . request (label, path, params : {}, method : ' GET ' , block : nil ) @request_threads << Thread .new do body = request_api(label, path, params, method, timeout, must) results = body[ :results ].present? ? body[ :results ] : [] block.call(results, body[ :info ], body) if block # コールバックに相当 end end def self . wait_all_requests @request_threads .each(& :join ) end end このコードで注意すべき点は、コールバック内で再びAPIリクエストが行われた際には、@request_threadsに新たなスレッドが追加された後に、そのスレッドが終了することです。それによって、wait_all_requestsは全てのAPIリクエストの完了を待つことができます。 複雑な依存関係 ページによっては、複数のAPIリクエストに依存する処理も存在します。そのような場合にも対応できるように以下のような処理を実装しました。 IqonModel .register_callback([ :item , :recommend_items ]) do # :itemと:recommend_itemsのAPIリクエストに依存する処理 end class IqonModel def self . init # ... @statuses = {} @callbacks = {} @callback_mutex = Mutex .new end def self . request (label, path, params : {}, method : ' GET ' , block : nil ) @statuses [label] = :run # ... fire_callback(label) end def self . register_callback (labels, &block) @callbacks [labels] = block end def self . fire_callback (label) @callback_mutex .lock @statuses [label] = :done @callbacks .select { | labels , _v | labels.include? label }.each do | labels , block | block.call if labels.all? { | l | @statuses [l].present? && @statuses [l] == :done } end ensure @callback_mutex .unlock end end 指定されたAPIリクエストが全て完了した時点で渡されたブロック内の処理が実行するために、 Mutexを用いて、並列で実行されるAPIリクエスト処理を確実に一つずつ終了状態に切り替えています。 まとめ Web APIを用いたアプリケーションで問題になりがちなリクエストの効率化について取り組みました。スレッドを使った並列化は場合によってはパフォーマンスの劣化につながることもありますが、今回のように、依存関係を適切に処理することで、パフォーマンスを向上させることができます。 最後に VASILYでは、iQONをよりよくするために新しい仕組みを一緒に作っていけるような仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 www.wantedly.com
アバター
2016年8月9日、スタートアップで活躍したいエンジニア向けのトークイベント、 CAREER TALK for Engineer を開催します。今回は MERY を運営する株式会社peroli様と弊社VASILYの2社での開催となります。堅苦しい会ではなく、お酒を飲みながらのトークイベントですのでお気軽にご参加ください。 イベント概要 日時:8月9日(火)19:30〜22:30 場所:渋谷ヒカリエ21F DeNAセミナールーム 参加対象者:スタートアップに興味のあるエンジニア(経験者・新卒問わず) 定員:50名様程度 参加登録(connpass) 何をやるイベントか スタートアップ企業を知ってもらい、興味を持って欲しいというのがイベントの狙いです。 実態が分からないために転職や就職に踏み切れていない方への後押しになればと思っています。 簡単な各社の会社説明の後、「ベテラン」「新卒」「女性」といったメンバーが「ベンチャーで働くメリット/デメリット」などを語ります。 ぜひ普段気になっていること、企業の会社説明会は聞きにくいこととなど、本音を聞き出してください! 皆様のご参加お待ちしております! 参加登録(connpass)
アバター
こんにちは、バックエンドエンジニアのjoeです。 みなさんはお気に入りのアプリに月額課金をしたことがありますか?したことがない人は今すぐお気に入りのアプリをみつけて月額課金しましょう! 実際にiOSで月額課金をすると、課金の証明としてAppStoreがレシートを発行します。レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。ユーザーが解約処理をしない限りAppStore側でレシートが自動更新される仕組みになっています。(月額課金の場合) その際に、AppStoreのサーバーにHTTPのPOSTリクエストでレシートを問い合わせ、現在の課金状況を知ることができます。このお問い合わせ処理と、レシートが不正なレシートでないかをチェックする処理を合わせてレシート検証と呼びます。 今回はiOSのレシート検証をクライアントのみでの検証からサーバーサイドでの検証に実装を変更したので、その理由や方法、考慮するべき点などを書きます。 注意 この情報は2016年8月3日現在のものです。 サーバーサイドでのレシート検証が推奨される理由 レシート検証は、クライアント側で完結させることもできます。しかし、クライアント側で完結させてしまうことで2つのデメリットが発生します。 クライアント側で検証処理が閉じているためユーザーによるレシート改ざんが可能になってしまう 複数のプラットフォームで課金サービスを提供している際に、クロスデバイス対応が難しくなる 1に関して、JailBreakやroot化を使って不正レシートをサーバーサイドに送りつける例があるそうなので、サーバーサイドでのレシート検証が推奨されています。 2に関しては、機種変更やデバイスの使い分けをしている方など、iOS/Android/Web等の複数プラットフォームで同じユーザーでログインする場合があります。その場合に、サーバーサイドに検証できる仕組みがあれば別端末にログインした場合でも、より正確な課金の有無の検証が可能になります。 レシート検証の方法 AppStoreのサーバーにHTTPのPOSTリクエストを送ることでレシートを問い合わせることができます。 リクエストURL テスト環境用のURLと、production用のURLで分かれています。 環境 URL 用途 production https://buy.itunes.apple.com/verifyReceipt 本番用 sandbox https://sandbox.itunes.apple.com/verifyReceipt 開発時のテスト環境用 sandboxにはsandboxの、productionにはproduction用のレシートがあり、productionのURLにsandbox用のレシートを送るとエラーが返ってきます。 sandboxを利用するには、Appleのテストアカウントを取得して課金処理のテストを行います。 リクエストbody 下記の2つだけでOKです。 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI... password アプリケーションの共有鍵 fea2ebde5... receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。 サーバーサイドでレシートをBase64エンコードするのではなく、クライアントかAppStoreからBase64エンコード済みのデータを受け取ります。一番最初の購入時に必ずクライアントからBase64エンコード済みのレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。 実装上の注意 審査が通るまではsandboxしか使えないのでproduction環境でもsandboxを受け付けられるような実装にしておく必要があります。 rubyのレシートサーバーサイド検証用のgemに venice というgemがありますが、AppStoreのAPIのバージョンによっては動きません。gemを使う場合は、ご自分のバージョンで正常に動作をするかしっかり確認してから使うことをおすすめします。 [2016/08/04:追記] veniceと別のgemを紹介していただきました!参考になれば幸いです。 https://github.com/mbaasy/itunes_receipt_validator サーバーサイドレシート検証の処理構成 月額課金購入時 更新時 弊社では課金処理を始めた当初、クライアント側のみで認証を行っており、途中でサーバーサイドの検証に変更したため、古くから課金しているユーザーはサーバーサイドにBase64エンコード済みのレシートを保持していません。なので、一度クライアント側に問い合わせてレシートを受け取ってから検証を行うようにしました。 アプリ立ち上げ時の課金状況の問い合わせを毎回行うと重くなるため、ユーザーテーブルに保持してある課金期限が過ぎているユーザーのみ行っています。 AppStoreから返ってくるレシート情報の項目 APIが返す項目の公式ドキュメントは下記のリンクです。 公式ドキュメント 注意しなければならないのが、古くから課金コンテンツを営んでいるアプリだとその当時のバージョンのAPIが返ってきます。弊社の課金コンテンツは2014年に開始されているためか、iOS6タイプのAPIが返ってきています。 その証拠に、 only returned for iOS6 と書いてある下記の2つの項目がAPIのレスポンスに含まれています。 途中でサーバーサイド検証に置き換えるような場合、最新ドキュメントの通りにAPIが返らない可能性があるため注意が必要です。ドキュメントだけを信頼せず、実際に返ってくるフィールドを見ながら実装をすすめることをおすすめします。(iOS7以降のAPIバージョンでもiOS6のみが返す項目と書いてある latest_receipt が返るという記事もあったので、公式ドキュメントの全てが正しい情報ではないかもしれません。) ※ [危険!!!!]iOS6タイプのトランザクション形式を使っている人への注意 receipt フィールドに含まれる in_app という項目は、公式ドキュメントの説明文には In the JSON file, the value of this key is an array containing all in-app purchase receipts. と書いてありますが、実際には1ヶ月毎の更新が少し遅れることがあったので最新レシートの認証を行う場合は latest_receipt_info のほうを使うことをおすすめします。こちらのほうはリアルタイムで更新されます。 検証内容 レシートの内容で検証すべき項目 項目 項目の内容 検証内容 status 0であれば正常なレシート、その他は不正なレシート( エラーコード表 参照) AppStoreから正常なレシートが返ってきているか in_app又はlatest_receipt_info 過去の購入履歴 課金履歴が存在しているか bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か transaction_id 1ヶ月ごとのレシート毎に発行される固有のid 別のユーザーのレシートを使っていないか original_transaction_id ProductごとのAppStore固有のid 同じAppStoreで別のアカウントが既に課金していないか expires_date レシートの期限 期限は切れていないか 各テーブルに保持すべき項目 ユーザーテーブルに持つべき項目 項目 内容 用途 purchase_device どのプラットフォームで課金しているユーザーなのか クロスデバイス対応の際に有用 premium_expire_time 課金の期限 レシート検証対象のユーザーかどうかをチェック レシートテーブルに持つべき項目 user_idとレシートの内容全部 サーバーサイドのレシート検証で考慮すべきこと クロスデバイス 状況 AndroidからiOSへの機種変更時(逆も然り) 複数デバイスでのログイン 対処方法 ユーザーテーブルに保持したpurchase_deviceを確認し、どのプラットフォームで課金しているユーザーなのかをチェック AppStoreのレシート検証が通らなかったら、別のデバイスの検証で課金の有無をチェック 例) ダブルアカウント 状況(ニッチな状況ですが) アカウントを2つ持っている 過去に片方のアカウントで課金していたことがある 現在つかっているAppStoreのアカウントが過去に課金していたAppStoreと同じアカウント original_transaction_idによる認証をサーバーサイドで行っている(original_transaction_idはProductごとにAppStoreで固有の値なので、前に課金していた時と再度課金する時のどちらも同じ値で返ります) もう片方のアカウントでもう一度課金したい 対処方法 original_transaction_idが一緒な他のユーザーが居ても、片方のアカウントでユーザーテーブルのexpires_dateが過ぎていれば認証OKにする 例) まとめ iOSのサーバーサイドレシート検証の実装のメリットから実際の検証方法、考慮する点をまとめました。 状況によって考慮すべき点や検証すべき項目、ハマるポイントが違うと思うので、公式ドキュメントはもちろんのこと、複数の実装事例の記事を読んでみると良いと思います。 最後まで読んでいただき、ありがとうございました! 参考資料 公式ドキュメント Validating Receipts With the App Store レシート検証プログラミングガイド ブログ 参考になったブログです。ありがとうございます! 自動購読課金について【iOS編】 iOS/Androidアプリ内課金の不正なレシートによる有料会員登録を防ぐ 最後に VASILYでは一緒に開発をしてくれる仲間を募集しています。 興味のある方は是非気軽にオフィス見学にお越しください! https://www.wantedly.com/projects/61389 www.wantedly.com Icon made by Freepik from http://www.flaticon.com/
アバター
iOSエンジニアの庄司( @WorldDownTown )です。 iQONのiOSアプリ内部で使われている画面遷移処理をOSSライブラリ化したのでご紹介します。 TL;DR UINavigationController での遷移時に、タップした画像をズームして遷移するトランジション処理をSwiftライブラリ化しました。 エッジスワイプでもズームアウトして戻ることができます。 github.com ライブラリ化した経緯 Pinterestをはじめ、画像がズームインしながら画面遷移するアプリは今や珍しくありません。 この表現を実現するライブラリはいくつか存在しますが、通常の UINavigationController のようにスワイプで戻れなくなったり、スワイプできても通常のスワイプとは違って指の動きに同期しないものが多い印象です。 iQONのアイテム詳細ページではこのジェスチャー周辺の実装がしっかりできているので、OSSとして公開したら需要があるかもと思い、ライブラリ化に踏み切りました。 特徴 エッジスワイプ 通常の UINavigationController と同様に、エッジスワイプで前のViewControllerに戻る事ができます。 上記のアニメーションGIFを見ていただくとわかりやすいと思います。 Objective-Cプロジェクトでも使えます 作成したクラスはすべて Foundation , UIKit のクラスを継承しているため、Objective-Cのコードからも利用できます。 使い方 このライブラリを使って画面遷移のアニメーションを実装するには3つのステップがあります。 UINavigationControllerDelegate設定 画面遷移元のViewController設定 画面遷移先のViewController設定 1. UINavigationControllerDelegate 設定 UINavigationControllerDelegate で画面遷移対象のViewControllerをチェックしてアニメーションをします。 ZoomNavigationControllerDelegate オブジェクトを delegate に設定するだけです。 class NavigationController : UINavigationController { private let zoomNavigationControllerDelegate = ZoomNavigationControllerDelegate() required init ?(coder aDecoder : NSCoder ) { super . init (coder : aDecoder ) delegate = zoomNavigationControllerDelegate } } 2. 画面遷移元のViewController設定 アニメーション対象の UIImageView を返したり、アニメーション中に元の画像を非表示にできるように画面遷移前後の ZoomTransitionSourceDelegate のメソッドを実装します。 extension ImageListViewController : ZoomTransitionSourceDelegate { // アニメーション対象のUIImageViewを返す func transitionSourceImageView () -> UIImageView { return selectedImageView } // スクリーンに対するアニメーション開始位置を返す func transitionSourceImageViewFrame (forward forward : Bool ) -> CGRect { guard let selectedImageView = selectedImageView else { return CGRect.zero } return selectedImageView.convertRect(selectedImageView.bounds, toView : view ) } // 画面遷移直前 func transitionSourceWillBegin () { selectedImageView?.hidden = true } // 画面遷移完了後 func transitionSourceDidEnd () { selectedImageView?.hidden = false } // 画面遷移キャンセル後 func transitionSourceDidCancel () { selectedImageView?.hidden = false } } 3. 画面遷移先のViewController設定 ZoomTransitionSourceDelegate と同様の目的で画面遷移先のViewController向けの設定のため、 ZoomTransitionDestinationDelegate のメソッドを実装します。 extension ImageDetailViewController : ZoomTransitionDestinationDelegate { // 画面遷移完了後、及び、ポップ時のUIImageViewの配置 func transitionDestinationImageViewFrame (forward forward : Bool ) -> CGRect { if forward { let x : CGFloat = 0.0 let y = topLayoutGuide.length let width = view.frame.width let height = width * 2.0 / 3.0 return CGRect(x : x , y : y , width : width , height : height ) } else { return largeImageView.convertRect(largeImageView.bounds, toView : view ) } } // 画面遷移直前 func transitionDestinationWillBegin () { largeImageView.hidden = true } // 画面遷移完了後 func transitionDestinationDidEnd (transitioningImageView imageView : UIImageView ) { largeImageView.hidden = false largeImageView.image = imageView.image } // 画面遷移キャンセル後 func transitionDestinationDidCancel () { largeImageView.hidden = false } } リポジトリに Demo プロジェクトがあるので、そちらもご覧ください。 ライブラリの内部実装 ズームアニメーションの仕組み UIViewControllerAnimatedTransitioning プロトコルを採用した ZoomTransitioning が画像がズームするアニメーション処理を実行しています。 UIViewControllerAnimatedTransitioning による画面遷移アニメーションについては、下記のQiita記事が参考になったので、そちらをご覧ください。 qiita.com スワイプで戻る UIPercentDrivenInteractiveTransition を継承し、 UIGestureRecognizerDelegate を採用した ZoomInteractiveTransition というクラスがスワイプによる画面遷移を実現させています。 let zoomInteractiveTransition = ZoomInteractiveTransition() let gesture = navigationController.interactivePopGestureRecognizer gesture?.delegate = zoomInteractiveTransition gesture?.addTarget(zoomInteractiveTransition, action : #selector(ZoomInteractiveTransition.handlePanGestureRecognizer(_ : ))) UINavigationController の interactivePopGestureRecognizer というプロパティは UIScreenEdgePanGestureRecognizer クラスで、このGestureRecognizerが通常のエッジスワイプで戻る動作を可能にしています。 ZoomInteractiveTransitioning で interactivePopGestureRecognizer のジェスチャー処理を受け取ることで、 ZoomTransitioning が実行するアニメーションを使ってエッジスワイプで戻れるようになります。 // UINavigationControllerDelegate func navigationController (navigationController : UINavigationController , interactionControllerForAnimationController animationController : UIViewControllerAnimatedTransitioning ) -> UIViewControllerInteractiveTransitioning ? { return zoomInteractiveTransition.interactive ? zoomInteractiveTransition : nil } エッジスワイプのジェスチャーを受け取ってナビゲーションを戻るときだけ zoomInteractiveTransition を返して、指の動きを反映したインタラクティブな画面遷移をします。 class ZoomInteractiveTransition : UIPercentDrivenInteractiveTransition { var interactive = false // スワイプで戻るフラグ。ジェスチャーを受け取ったらtrueにする @objc func handlePanGestureRecognizer (recognizer : UIScreenEdgePanGestureRecognizer ) { let view = recognizer.view ! let progress = recognizer.translationInView(view).x / view.bounds.width switch recognizer.state { case .Changed : updateInteractiveTransition(progress) case .Cancelled, .Ended : if progress > 0.33 { finishInteractiveTransition() } else { cancelInteractiveTransition() } default : break } } UINavigationController の interactivePopGestureRecognizer のジェスチャーで呼ばれるメソッドの方では、スワイプする指の位置ごとに、 UIPercentDrivenInteractiveTransition のメソッドを呼び出してアニメーションの進捗を反映させます。そうすると、 ZoomTransitioning のアニメーションが指の位置に合わせて動作します。 複雑な処理に見えますが、もし UIPercentDrivenInteractiveTransition を使わなかった場合、スワイプで戻る時も ZoomTransitionig と同じアニメーション処理を別途実装しないといけません。 さいごに UINavigationController での遷移時に、タップした画像をズームインしながらアニメーションする ZoomTransitioning を紹介しました。 このライブラリは、iQONでの仕様を基に最低限の機能で公開しています。 バグ報告や改善案など (OSS化の真の目的はこれだったり…)、 Pull Request お待ちしております。
アバター
こんにちは、iOSエンジニアの遠藤です。 学生の皆さん、夏のインターンシップはもう決めましたか? 各社で様々な形式のインターンシップがあると思いますが、今回はiOSチームを例にVASILYでのインターンシップについて紹介をしたいと思います。 記事には実際にインターンシップに参加した学生の感想を載せていますので、VASILYのインターンシップに興味のある方はぜひチェックしてください! VASILYでのインターンシップについて VASILYのインターンシップの特徴は、なんといっても実際のプロダクトの開発をしてもらうことです。 講義形式やハッカソン形式のインターンシップがあるなか、なぜ実際のプロダクトを開発する形式のインターンシップを行っているのでしょうか? それは、実際のプロダクトを開発する中でエンジニアとしてのスキルだけではなくコミュニケーション力が必要になることや実際にプロダクトを使っているユーザーに価値を届けることの難しさや楽しさを実感してもらいたいと思っているからです。 また、一緒にプロダクトを開発することでVASILYのエンジニアがどういった働き方をしているのかやプロダクトへの思いなども知ってもらいたいと思い、このような形式にしています。 インターンシップの参加条件 インターンシップへの参加条件として、こちらが出す課題をクリアしてもらう必要があります。 プログラミングの勉強に時間をかけるのではなく、ユーザーに価値を届けることに時間を使って欲しいのでVASILYではプログラミングを一から教えるといったことはしません。 実際のプロダクトを開発するので、難しい内容や課題を解決するのにある程度の技術力が必要になってきます。 その中でも、きちんとアウトプットとしてユーザーに価値を提供し成果を上げてもらいたいので、ある程度開発の経験がある方を対象とするために課題を出しています。 また、技術力だけではなく以下のような項目も重要視しています。 プログラミングがとにかく好きか、ものづくりが大好きか 素直で吸収力があるか 技術の力で世の中をもっと便利にしたいと思っているか 数百万人が使うプロダクトの設計や技術、開発スキルに興味があるか インターンシップで学べること VASILYのインターンシップで学べることをiOSを例に紹介したいと思います。 【iOSインターンシップで学べること】 大規模プロダクトでの開発経験 コーディング力 コミュニケーション力 UI実装 大規模プロダクトでの開発経験 インターンシップでは、実際にiQONのプロダクトに関する開発をしてもらいます。 大規模なプロダクトでの設計についてや、コードに対するPull Requestは個人の開発では学べないことを学べます。 コーディング力 小さなコードの変更でも、必ずコードレビューをします。 実装するだけでなくアプリ開発の作法やコードの書き方などしっかりと指摘していくので、コーディング力は上がると思います。 ↓ 過去、インターンに来てくれた学生のPull Requestの様子です。 細部までコードをチェックしていきます。 コミュニケーション力 実装する上で、デザインの確認や仕様の認識を合わせるためにデザイナーや企画の人ともコミュニケーションをとってもらいます。 ただ実装するだけではなく、デザインや企画などの調整もしてもらうのでコミュニケーション力はつくと思います。 UI実装 iQONはデザインにこだわって作られています。 デザイナーが作ったデザインは簡単に実装できるものではありません。 そのデザインを忠実に再現するためにAutoLayoutを駆使して実装をします。 一筋縄では実装できないものが多いので、UIの実装力はあがると思います。 参加者の感想 大阪大学Hくん インターンシップを通して技術的な事は勿論、チームみんなで良いプロダクトを作るために自分は何をすべきなのかを、実際に多くのユーザーがいる大規模なアプリの開発を通して体験しながら学べました。 アプリ開発のメンバーだけではなく、ウェブ開発の人やマーケティングの人、経営側の人とも話をできてとても楽しいインターンシップでした。 早稲田大学大学院Tくん チームとしての開発を経験できるだけでなく、プロダクトに対する考え方も身につくことが出来るため、とても実践的で魅力ある内容でした。これまでの自分は個人での開発がほとんどだったので、VASILYでのインターンシップは得るものが多く、エンジニアとしての自分の糧となりました。 インターンシップからの内定 VASILYでは新卒採用の選考フローにインターンシップへの参加が入っているため、新卒入社するためにはインターンシップの参加が必須になっています。 私自身もVASILYのインターンシップを経て新卒で入社しました。 実際のプロダクトを開発するインターンシップの魅力はその会社の働き方や開発している人たちのプロダクトに対する思いなどを知れることだと思います。それは入社した後に想像と違ったというようなミスマッチを防げます。 最後に iOSチームを例にVASILYでのインターンシップについて紹介しました。 実際のプロダクトに携わることで技術力だけではなく、チームでの開発の進め方やプロダクトに対する考え方を学ぶことができます。 色々な形式のインターンシップがあると思いますが、実際のプロダクトを開発するインターンシップには成長するチャンスがたくさんあります! チーム開発をしてみたい方、プログラミングが好きな方、iQONをつくってみたい方、ぜひともVASILYのインターンシップに応募してみてください。 https://www.wantedly.com/projects/54278 www.wantedly.com
アバター
VASILYのiOSエンジニアにこらすです! 今回はプロキシツール mitmproxy の カスタムスクリプト 機能について説明したいと思います。 モバイル開発をする際にAPIリクエストのデバッグツールとして mitmproxy はとても役に立ちます! カスタムスクリプトを使うと何ができるか カスタムスクリプトを書くと、mitmproxyの動作をプログラムでカスタマイズすることができます。 モバイルデバイスのリクエストやレスポンスを書き換えたり、通信を遅延させたりできます。 どうやってスクリプトを書くか mitmproxyはPythonで実装されているので、カスタムスクリプトも python で実装することになります。 mitmproxyにはいろいろなイベントがありますが、各イベントをオーバーライドするメソッドを書くことができます。 例えば、 script_name.py というPythonファイルにオーバーライドしたい処理を書いたなら、mitmproxyの -s オプションでそのファイルを指定するとカスタムスクリプトが動くようになります。 $ mitmproxy -s script_name.py イベントをオーバーライドするメソッドについて mitmproxyのイベントには4つの種類があります。 ライフタイムイベント(スクリプト開始、終了) ネトワーク接続系イベント(クライアントのconnect、disconnect) TCP系イベント HTTPイベント 今回は、APIのデバッグ時によく使う HTTPイベント に注目します。 HTTPイベントメソッドオーバーライド request(context, flow) クライアントがリクエストをする直前にこのメソッドが実行されます。 このタイミングでリクエスト情報をチェックできますし、編集もできます! response(context, flow) このメソッドはサーバーから受け取ったレスポンスを編集することができます。 responseheaders(context, flow) このメソッドはサーバーからレスポンスヘッダを受け取った直後に実行されます。 response(context, flow) より前に responseheaders(context, flow) が実行されることが保証されているので、レスポンスボディを受け取る前にレスポンスヘッダを書き換えることができます。 error(context, flow) エラーが発生した場合にこのメソッドに入ります。 mitmproxyのインラインスクリプトの実例 オーバーライドするための基礎を勉強したので今からちゃんとmitmproxyスクリプトを書いてみましょう! ヘッダを書き換える # リクエストヘッダを編集 def request (context, flow): flow.request.headers[ "language" ] = "jp" # レスポンスヘッダも同じです def response (context, flow): flow.response.headers[ "language" ] = "jp" URLのパスが /users のリクエストだけ強制的に 451 エラーにする def response (context, flow): if "/users" in flow.request.url: flow.response.status_code = 451 60秒レスポンスが返ってこない状況をシミュレートする import time def response (context, flow): time.sleep( 60 ) URLのパスが /users のリクエストのレスポンスのボディを置き換える def response (context, flow): if "/users" in flow.request.url: flow.response.content = b "" # bytesタイプのascii文字列 まとめ 今回はmitmproxyのカスタムスクリプトについて説明しました。 このたった4つのスクリプトを覚えるだけでも、APIの問題を解決できるようになると思います。 もっと詳しい情報や他のカスタムスクリプトについては、mitmproxyの 公式サイト か GitHub を参照してください。 ありがとうございました! ー にこらす
アバター
こんにちは、インフラエンジニアの光野(@kotatsu360)です。 開発をしていると本番サーバと開発サーバの乖離が問題になると思います。これについて、先日行われた UZABASE Meetup#4 〜大規模サービスを支えるインフラ〜 にて「1コマンドで本番サーバと開発サーバ (のVMイメージ)を作る話」という発表をさせていただきました。 この記事では、時間とスライドの都合上、省略したbase.jsonについてご紹介いたします。 packer build base.json packerで読み込むjsonは次の4パートに分かれています。 " variables ": { // 変数 } , " builders ": [ // 作成したいプラットフォームごとの設定 ] , " provisioners ": [ // マシンイメージへの初期設定 chef, shell script, ansible ... ] , " post-processors ": [ // 作成したマシンイメージへの後処理 ] 非常にシンプルなのですが、実際に設定していくと細かなパラメータの設定で悩みます。ということで実際に使っているjsonファイル全文をこちらにご用意しました! packer/base.json { " variables ": { " version ": " 1.0.2 ", " role ": " base ", " ami ": " ami-5d38d93c ", " aws_access ": " {{ env `AWS_ACCESS_KEY_ID`}} ", " aws_secret ": " {{ env `AWS_SECRET_ACCESS_KEY`}} ", " gce_source_image ": " ubuntu-1604-xenial-v20160627 ", " gce_secret ": " {{ env `GCE_ACCOUNT_FILE`}} ", " s3_bucket ": " {{ env `AWS_INFRA_S3_BUCKET`}} ", " gce_project_id ": " {{ env `GCE_PROJECT_ID`}} " } , " builders ": [ { " type ": " virtualbox-ovf ", " headless ": " true ", " shutdown_command ": " echo 'ubuntu' | sudo -S shutdown -P now ", " source_path ": " box-source/ubuntu-16.04.ova ", " ssh_password ": " ubuntu ", " ssh_username ": " ubuntu ", " ssh_wait_timeout ": " 20m ", " vboxmanage ": [ [ " modifyvm ", " {{ .Name }} ", " --memory ", " 4096 " ] , [ " modifyvm ", " {{ .Name }} ", " --cpus ", " 2 " ] ] , " virtualbox_version_file ": " .vbox_version ", " vm_name ": " {{user `role`}} ", " guest_additions_mode ": " disable ", " format ": " ova ", " output_directory ": " output-{{build_name}}-{{user `role`}} " } , { " type ": " amazon-ebs ", " access_key ": " {{user `aws_access`}} ", " secret_key ": " {{user `aws_secret`}} ", " source_ami ": " {{user `ami`}} ", " instance_type ": " c3.xlarge ", " region ": " ap-northeast-1 ", " ssh_username ": " ubuntu ", " ami_name ": " packer-ubuntu1604-ruby231-{{timestamp}} ", " ami_regions ": [ " ap-northeast-1 " ] , " ami_description " : " iQON AMI {{user `role`}} Image ", " tags ": { " OS ": " Ubuntu16.04 ", " Ruby ": " 2.3.1 ", " Role ": " {{user `role`}} ", " OriginalAMI ": " ami-5d38d93c " } }, { " type ": " googlecompute ", " account_file ": " {{user `gce_secret`}} ", " project_id ": " {{user `gce_project_id`}} ", " source_image ": " {{user `gce_source_image`}} ", " zone ": " asia-east1-a ", " machine_type ": " n1-highcpu-4 ", " ssh_username ": " ubuntu ", " instance_name ": " packer-{{timestamp}} ", " image_name ": " packer-ubuntu1604-ruby231-{{timestamp}} ", " image_description " : " iQON AMI {{user `Role`}} Image " } ], " provisioners ": [ { " type ": " file ", " source ": " {{pwd}}/../chef-repo ", " destination ": " /tmp/packer-chef-client/ " } , { " type ": " shell ", " inline ": [ " sudo apt-get update ", " sudo apt-get upgrade -y ", " sudo apt-get install -y language-pack-ja curl ", " sudo update-locale LANG=ja_JP.UTF-8 && true ", " sudo ln -sf /bin/bash /bin/sh " ] } , { " type ": " chef-client ", " server_url ": " http://localhost:8889 ", " config_template ": " ../chef-repo/client.rb ", " install_command ": " curl -L https://www.chef.io/chef/install.sh | sudo bash -s -- -v 12.8.1 ", " execute_command ": " sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json ", " guest_os_type ": " unix ", " skip_clean_node ": true , " skip_clean_client ": true } , { " type ": " shell ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " sudo systemctl disable apt-daily.service ", " sudo systemctl disable apt-daily.timer " ] } ] , " post-processors ": [ { " type ": " shell-local ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}} ", " aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source " ] } , [ { " type ": " vagrant ", " only ": [ " virtualbox-ovf " ] , " keep_input_artifact ": false , " output ": " packer-output/{{user `role`}}/{{user `role`}}.box ", " override ": { " virtualbox ": { " compression_level ": 0 } } } , { " type ": " vagrant-s3 ", " only ": [ " virtualbox-ovf " ] , " region ": " ap-northeast-1 ", " bucket ": " {{user `s3_bucket`}} ", " manifest ": " vagrant/json/{{user `role`}}.json ", " box_name ": " {{user `role`}} ", " box_dir ": " vagrant/boxes ", " version ": " {{ user `version` }} ", " acl ": " private ", " access_key_id ": " {{user `aws_access`}} ", " secret_key ": " {{user `aws_secret`}} " } ] ] } シークレットや組織固有の部分については環境変数を読み込むようにしています。また実行時に引数として渡す事も可能です。 User Variables in Templates - Packer by HashiCorp base.jsonの中身 packerはinspectというサブコマンドでそのJSONで何が実行されるのかが確認できます。base.jsonを見てみます。 $ packer inspect base.json Optional variables and their defaults: ami = ami-5d38d93c aws_access = {{ env `AWS_ACCESS_KEY_ID`}} aws_secret = {{ env `AWS_SECRET_ACCESS_KEY`}} gce_project_id = {{ env `GCE_PROJECT_ID`}} gce_secret = {{ env `GCE_ACCOUNT_FILE`}} gce_source_image = ubuntu-1604-xenial-v20160627 role = base s3_bucket = {{ env `AWS_INFRA_S3_BUCKET`}} version = 1.0.2 Builders: amazon-ebs googlecompute virtualbox-ovf Provisioners: file shell chef-client shell このbase.jsonでは、9個のユーザ定義変数で動作が制御されており、 ami (EBS-attached) google compute engine image virtualbox ovf (vagrant box用) が作られ、それぞれプロビジョンは file copy remote shellの実行 chef client remote shellの実行 という順番で行われる。ということが分かります。もう少し分解して見ていきます。 variables AWSのトークンやオリジナルAMI (ここではUbuntu16.04) を設定します。 複数回登場する要素はvariablesで定義しておいた方が見通しが良くなります。 シークレットをファイルに含めなくて済むのでGitHubにコミットするときも安全です。 " variables ": { " aws_access ": " {{ env `AWS_ACCESS_KEY_ID`}} ", " aws_secret ": " {{ env `AWS_SECRET_ACCESS_KEY`}} " } , builders Vagrant boxの元となるVirtualBoxと本番で使うAMI/GCE Imageを作っています。 " builders ": [ { " type ": " virtualbox-ovf ", " source_path ": " box-source/ubuntu-16.04.ova ", ... } , { " type ": " amazon-ebs ", ... } , { " type ": " googlecompute ", ... ] AMI/GCE Imageについては見たままです。各プラットフォームが公式で提供しているUbuntu16.04のイメージを使ってインスタンスを立て、後述のプロビジョニングを行い、マシンイメージを保存してインスタンスを削除してくれます。 一方、VirtualBoxについては一手間かけています。 source_path で指定しているovaは Canonicalが提供しているiso を一度VirtualBoxに入れて、OVF2.0でエクスポートしたものです。 Ubuntu14.04だと Canonical公式のbox をtarで解凍したときに出てくるovaをpackerで読み込めたのですが、 Ubuntu16.04のbox から同様に作成したovaは読み込みエラーになったため自分で作成しました。 provisioners " provisioners ": [ { " type ": " file ", " source ": " {{pwd}}/../chef-repo ", " destination ": " /tmp/packer-chef-client/ " } , { " type ": " shell ", " inline ": [ " sudo apt-get update ", " sudo apt-get upgrade -y ", " sudo apt-get install -y language-pack-ja curl ", " sudo update-locale LANG=ja_JP.UTF-8 && true ", " sudo ln -sf /bin/bash /bin/sh " ] } , { " type ": " chef-client ", " config_template ": " ../chef-repo/client.rb ", " execute_command ": " sudo chef-client -z -c /tmp/packer-chef-client/client.rb -j /tmp/packer-chef-client/nodes/packer-{{user `role`}}.json " , } , { " type ": " shell ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " sudo systemctl disable apt-daily.service ", " sudo systemctl disable apt-daily.timer " ] } ], packerで一番ハマったのがこのprovisinersです。大きな流れは、 chefで使うファイルをローカルからVMにコピー 設定をしておかないとそもそもchefが実行できない処理をremote shellで実行 chef clientをlocal modeで実行 virtualbox-ovf 限定で、インスタンス起動時のapt-get updateを停止 ということをやっています。4番目は発表資料の23ページ目で触れているapt-getがchefと衝突するのを避けるためです。 ちなみに、3で参照しているclient.rbの中身はこうなっています。1でコピーしたchef-repoの構造を指示しています。 # coding: utf-8 chef_repo_path " /tmp/packer-chef-client " cookbook_path [ " /tmp/packer-chef-client/site-cookbooks " , " /tmp/packer-chef-client/cookbooks " ] log_location " /var/log/chef-client.log " log_level :info post-processors " post-processors ": [ { " type ": " shell-local ", " only ": [ " virtualbox-ovf " ] , " inline ": [ " rsync --checksum -av output-virtualbox-ovf-{{user `role`}}/ box-source/{{user `role`}} ", " aws s3 sync box-source s3://{{user `s3_bucket`}}/vagrant/box-source " ] } , [ { " type ": " vagrant ", " only ": [ " virtualbox-ovf " ] , ... } , { " type ": " vagrant-s3 ", " only ": [ " virtualbox-ovf " ] , " manifest ": " vagrant/json/{{user `role`}}.json ", ... } ] ] post-processorsはbuildersで作成したVMイメージに対して後処理を行うことができます。ここでは、 virtualbox-ovf の結果を使って、vagrant boxを作成しています。 できたものはS3に保存し、チーム全員で共通のboxを使えるようにしています。この管理方法についてはこちらの投稿を参考にさせていただきました。 Packer で開発環境の Vagrant Box を自作して、post-processors 処理を通して S3 に保存・バージョン管理・ホスティングする - Qiita その他補足 トークンの権限について AWS/GCEのトークンに必要な権限については、公式ドキュメントで丁寧に紹介されていますので上では説明を省略しています。 Amazon AMI - Builders - Packer by HashiCorp Google Compute - Builders - Packer by HashiCorp ビルド対象について packer build base.json と実行すると3つのプラットフォームでビルドが始まりますが、対象を絞ることもできます。 packer build -only='amazon-ebs' base.json この場合、AMIだけビルドが始まります。 packer build - Commands - Packer by HashiCorp AWSのインスタンスタイプについて packerというよりもAWSの話題になりますが、検証中Ubuntu16.04 + {m,c,r}3.largeのインスタンスの場合にカーネルパニックが発生し正常に起動しないという問題がありました。 こちらのissueに近いのですが詳細が確認できず、largeを使わないという対応策を取っています。 Bug #1573231 “Kernel Panic on EC2 After Upgrading from 14.04 to ...” : Bugs : linux package : Ubuntu もう直っているかもしれませんがお気をつけ下さい。 現状の制約 このbase.jsonを使う際、2つ解決できてない問題があります。 一つ目: virtualbox-ovf の一時ファイル packerは一時ファイルが残っていると実行時にエラーが発生します。 基本動作としては消えるはずなのですが、post-provisionersの書き方が悪いのか、ある時から消えなくなってしまいました。 # 2回目の実行 $ packer build base.json virtualbox-ovf output will be in this color. Build 'virtualbox-ovf' errored: Output directory exists: output-virtualbox-ovf-base Use the force flag to delete it prior to building. 手動でディレクトリを消すか、 -force を付けてbuildを実行して下さい。 packer build -force base.json 二つ目:バージョンの手動変更 vagrant boxの管理でmanifest.jsonを内部で生成しており、既に存在するバージョンの場合は最後の vagrant-s3 が失敗します。 " variables ": { " version ": " 1.0.2 ", vagrantの仕様上、 x.x.x の形式にする必要があり、タイムスタンプというわけにもいきません。人間インクリメントなのでなんとかしたいと思っています。 今後やりたいこと まだまだこなれておらず、日々jsonを更新しています。 例えば、EC2に関してはスポットインスタンスを使うよう修正をしているところです。 ハイパフォーマンスのインスタンスが手頃な値段で使えるため、より快適なイメージ更新作業ができると思っています。 まとめ かなり駆け足ではありましたが、VASILYで実際に運用しているpacker用のJSONファイルについてご紹介いたしました。 packerは簡単に始められますが、ちょっと凝ったことをしようと思うとやはりオプションの調整が避けられません。 この記事が何かしら参考になれば幸いです。 逆に「お前のpacker術は間違っている」という部分がありましたら、コメントでご指摘下さい。小躍りして喜びます。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。バックエンドチーム(インフラはバックエンドチームに所属しています)でのインターンシップについては、以下のリンクで募集しています。ご興味のあるかたは是非ご応募下さい。
アバター
こんにちは、エンジニアの中村( @tn1031 )です。弊社のプロダクト「iQON」には「for You」というレコメンド機能が実装され、個々のユーザに毎日おすすめのファッションアイテムを届けています。 press.vasily.jp 今回はこの「for You」に関連して、レコメンドを実現するアルゴリズムのひとつである Bayesian Personalized Ranking (BPR) を紹介したいと思います。 本記事ではひとつの手法に話題を絞りますが、一般的な協調フィルタリングやレコメンド自体について詳しく知りたい方は、こちらの Netflix Prizeで使われた手法のまとめ がとても参考になります。 協調フィルタリングとBPR 行動ベースの協調フィルタリングではユーザ x アイテムの行列の行列分解(Matrix Factorization)を考えます。 評価の行列を とします。行のインデックス と列のインデックス がそれぞれ1人のユーザ、1個のアイテムと対応しており、行列の 成分の値 はユーザ がアイテム に対して下した評価です。ファクターの個数を与えた時、評価の行列 をユーザ x ファクター行列 とアイテム x ファクター行列 の積に分解します。 これを解く為の手法は様々ありますが、有名なものといえば spark.mllib にも実装されているAlternating Least Squares (ALS)やトピックモデルが一番最初に思い浮かびます。 ALSは観測されたデータと予測した評価値の2乗誤差を最小にするような行列分解を与える手法であり、トピックモデルは観測されたデータの背後に確率分布を仮定し、分布のパラメータを求める事で行列分解を行います。 BPRも行列分解を与える部分は同じですが、上記の手法とは異なるアプローチをとります。 そもそもPersonalized Rankingは、ユーザごとの趣味趣向をランキングとして学習します。アイテムリストをユーザの好みでソートしたリストは、結果としてそのユーザに対するレコメンドになっているということです。BPRはPersonalized Rankingを解くための枠組みであり、今回紹介するのはこの手法を行列分解に適用した場合のアルゴリズムです。 BPRの詳細 ユーザ x アイテム の行列の行列分解の問題をBPRで解くことを考えます。 はじめに扱うデータを定義します。続いてベイズ的アプローチによって問題を定式化し、パラメータの更新式を導出します。 データ BPRで扱う学習データは以下のように表現します。 はすべてのアイテムの集合、 はユーザ が好む(評価が正の)アイテムの集合、 は から を除いたものの集合です。したがって、 の意味は、「ユーザ はアイテム よりアイテム を好む」となります。 のサイズは になります。すべてのデータを学習に用いることは不可能なので、BPRでは学習データを与えられたデータからサンプリングします。 # sample a user u = np.random.randint(userCount) itemList = trainMatrix.getrowview(u).rows[ 0 ] if len (itemList) == 0 : continue # sample a positive item i = random.choice(itemList) # sample a negative item j = np.random.randint(itemCount) while trainMatrix[u, j] != 0 : j = np.random.randint(itemCount) 定式化 分解後の行列を (ただし、 はファクターの数)、ユーザ についての全アイテムの順序を 、 と表記すれば「ユーザ はアイテム よりアイテム を好む」ことを表すとします。 尤度関数は、 と定義します。ここで、 です。 の事前分布を を単位行列として で定義すると、事後確率最大化の式は以下で与えられます。 この式を最大化する を求めることがBPRの目的となります。 第1項で好きなアイテムに関する予測値とそうでないアイテムに関する予測値の差を大きくするように学習します。第2項、第3項は正則化項として機能します。 更新式 勾配法で を求めます。 とおくと、 を学習率として、 となります。 として を代入すると、最終的に以下のようになります。 ただし、 です。 ここまでをコードに起こします。 # BPR update rules y_pos = np.dot(W[u], H[i]) # target value of positive instance y_neg = np.dot(W[u], H[j]) # target value of negative instance exp_x = np.exp(-(y_pos-y_neg)) mult = -exp_x / ( 1.0 + exp_x) for f in xrange (factors): grad_u = H[i, f] - H[j, f] W[u, f] -= lr * (mult * grad_u + reg * W[u, f]) grad = U[u, f] H[i, f] -= lr * (mult * grad + reg * H[i, f]) H[j, f] -= lr * (-mult * grad + reg * H[j, f]) アルゴリズム データのサンプリングとパラメータの更新を収束するまで交互に繰り返します。 repeat 1. (u,i,j)のサンプリング 2. W,Hの更新 until convergence コードの全量は GitHub に公開しておきます。 なお、上記のコードをそのまま実行するとあまりに遅かったため、一部実装を見直しました。 そのときの試行錯誤は Qiita に書いたのでよろしければご覧ください。 BPRの魅力 BPRの魅力は何と言っても計算コストです。 をそれぞれユーザ数、アイテム数、評価の数、ファクターの数とすると、例えばALSは 、速いものだと であるのに対し、BPRは で計算できます。 また、目的関数やアルゴリズムがシンプルで拡張を考えやすい事もメリットといえます。例えば、行動データの他にアイテムの画像情報をモデルに取り入れたいときは こちらの論文 で提案されているような拡張が考えられます。 一方、ALSに比べると並列化が難しいです。ファクター毎であれば可能ですが、ALSの様に全ユーザ/アイテムに対して並列計算させるのは大変かもしれません。 計算資源を惜しみなく投入できる環境であれば、分散環境下において近似なしで計算可能なALSは非常に強力ですが、頻繁に更新したい・1回あたりの計算コストを(金額的にも時間的にも)抑えたいという要件があればBPRも選択肢に含まれると思います。 まとめ 軽量なレコメンドアルゴリズムのひとつであるBPRを紹介しました。Personalized Rankingをベイズ的に定式化したもので、行列分解に適用することでレコメンドを達成します。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。データサイエンスチームでのインターンシップについては、以下の記事で紹介していますので、ご興味のあるかたは是非ご覧ください。 tech.vasily.jp
アバター
エンジニアの荒井です。現在VASILYでは サマーインターンシップ を開催しています。募集開始後、さっそく多くの方からご応募いただいています。 インターンコースのひとつにフロントエンド開発コースがあるのですが、HTMLを書くのか、サーバーサイド言語を書くのか等、業務範囲に興味がある方が多いようです。そこで今回は、VASILYフロントエンドチームの役割と、インターンシップの内容について紹介したいと思います。 VASILYでのフロントエンドエンジニアの役割 はじめにVASILY内でのフロントエンドエンジニアの役割についてご紹介します。 フロントエンドエンジニアの役割は会社によって様々だと思いますが、VASILYのフロントエンドエンジニアは以下の技術を用いてプロダクト開発をする役割を担っています。 Ruby JavaScript HTML CSS フロントエンドエンジニアと聞いて、RubyやPHPといったサーバサイド言語を扱わないとイメージする方、サーバーサイド言語を用いるがHTMLやCSSはデザイナーがコーディングするというイメージを持って応募される方も多いですが、VASILYではフロントエンドエンジニアが両方のコーディングを担当しています。 もちろんインターンシップでも幅広く経験して頂こうと思っていますので、「Webサービスを作ってみたい!」と思われている学生の方にオススメです。 フロントエンドインターンシップの特徴 フロントエンドチームのインターンシップでは、iQONのPC/スマートフォンサイトの開発を担当していただきます。VASILYのメイン事業であるiQONの開発を実際に行い、インターンシップ中に本番環境へのデプロイまでを体験できます。短期のインターンシップで大規模なサービスに触れることが出来るのが大きな特徴のひとつです。 また、VASILY内でのフロントエンドエンジニアは、デザイナーやディレクターとのやり取りが多く行われる職種でもあります。エンジニア以外とのチーム開発に興味がある方にはとても良い経験になるはずです。 参加条件 フロントエンドチームのインターンシップ参加には課題の提出が必須となります。 内容は簡単なWebアプリケーション作成です。約10日間という短いサマーインターンシップ期間にデプロイまで経験していただくということから、課題を設けています。 作成したアプリケーションとソースコードを確認し、インターンシップ受け入れの合否が出ます。課題が問題なくクリア出来れば、Web業界での経験が無い初心者でも構いません。 過去のインターンシップ事例 過去に多くの学生に参加していただきましたが、2つほど実例を紹介します。 スマートフォンサイトのリニューアル PCトップページのリニューアル どちらの学生にもリニューアルという大きな開発を経験していただきました。 現在のPCトップページはインターンシップの学生が作成したものです。 インターンシップでは1人以上のメンターがつくので、やりがいのある仕事を、しっかりサポートを受けながら、経験することができます。 インターンシップからの内定 実は上記で紹介したリニューアルを経験した2名ですが、今ではVASILYのフロントエンドチームとして大活躍しています。短期インターンシップが終わった後、長期インターン、アルバイトを経験し選考へ進みました。 しっかりとした就業体験をしているので、企業と学生のミスマッチが無いのが内定直結型のインターンの良い所だと思います。 最後に 今回はフロントエンドチームのインターンシップの紹介をしました。 インターンシップで実際の業務を経験することで、普段VASILYのエンジニアがどのように仕事をしているか、企業文化やメンバーの雰囲気まで感じてもらえるはずです。 Webエンジニアを目指している方は、是非この夏VASILYのサマーインターンシップにご応募ください。 https://www.wantedly.com/projects/54278 www.wantedly.com
アバター
データサイエンスチームの後藤です。 学生のみなさんはそろそろ夏のインターンの時期ですね。 私も、ちょうど一年前に学生の立場でVASILYのインターンに参加して熱い夏を過ごしたことを思い出します。 本記事では、データサイエンスチームの実際の仕事と夏のインターンについてご紹介します。 記事の最後に、インターン募集の案内も貼っていますので、インターンに参加したいと思ってくれた方はぜひチェックしてください! VASILYのインターンの特徴 エンジニア向けのインターンでは、VASILYのプロダクトであるiQONに直接関わる開発を行っていただきます。よくあるコンペティション形式やハッカソン形式のようなものではなく、メンターと一緒に、実際のプロダクトで動いているコードを触りながら開発を進めていきます。私たちはユーザーに価値を届けることをとても大事にしていますので、インターン生にも、最後まで責任をもってアウトプットの質を高めてユーザーに届けてもらいます。作りっぱなしではなく、必ずユーザーの反応が返ってくるのでやり甲斐があります! インターンへの参加方法 VASILYのインターンに参加するためには、こちらが出す課題を解いてもらいます。課題は汎用的な内容で、とあるデータセットで推薦システムを作るというものです。詳細は応募してからのお楽しみですが、インターンで取り組んでもらう仕事の、準備研究に位置付けています。 課題では、問題設定の理解、アルゴリズムの実装など様々なスキルを測ります。これらはデータサイエンティストの実務での必須能力なので、課題に取り組む中でスキルをどんどん磨いてみてください! データサイエンティストの業務内容 インターン中はデータサイエンスチームと共に過ごすので、社員の業務に触れることもあるでしょう。 VASILYのデータサイエンティストの業務は大きく分けて「データ分析」と「研究開発」の二つがあります。 データ分析 データを目的に応じて適切に集計・可視化していきます。VASILYではユーザーの行動データ、商品データ、検索キーワードなど多種多様のデータを保有しています。それらを分析して仮説の検証、営業資料の根拠、異常検知に利用し、それに続く意思決定を支援します。エンジニアだけでなく、営業チーム、ビジネスチーム、経営陣などすべての部署と直接関わっていきます。これは大企業のデータサイエンティストと異なる部分かもしれませんね。現在は、各部署が追っている200を超える指標を常時最新の状態に保ち、社内に共有して役立てています。 日々追っている指標をTableauで可視化した例 研究開発 VASILYではiQONの新サービスの開発・改善のプロジェクトを複数進めています。目的の機能をつくるために、アルゴリズムの選定・実装をし、実データで検証、最終的に実際に稼働するサービスに仕上げていきます。開発言語は主にPythonを用いますが、webとの連携が強い部分はRubyを使って開発をすることもあります。 例えば、機械学習の国際会議の一つ、ICML2015で発表された多腕バンディッドのアルゴリズムがサービスとして稼働しており、Rubyでの実装も GitHub で公開しています。他にもiQONのサービスの裏側では、ディープラーニング、協調フィルタリング、LDAなどの機械学習の手法を活用しています。 今回のインターンでは、データサイエンスチームの一員として、研究開発の仕事をしてもらいます。定期的なミーティングや突発的に起こる議論にも参加していただきますので、本当の就労体験ができるでしょう。 私たちがどのような思いで仕事をしているのかに答えた記事もありますので、合わせてご覧ください! ビッグデータがあなたのファッションセンスを丸裸にする!ファッションアプリiQONを駆動させる最新データサイエンスの世界 去年の参加者の声 慶應義塾大学 Nさん VASILYのインターンでは実際に使われているデータを使って、実際のサービスに活かす分析が求められるので、非常に実践的で、分析の手法やツールはもちろん分析に対する考え方の面で大変勉強になりました。何よりファッションの大規模データという普段触れないデータを思い切り分析することができてとても楽しかったです! 東京大学大学院 Gさん VASILYのデータはとても整理されていて、分析がしやすい環境が整っていました。環境について不満がない分、自分の知識の足りなさ、実装力など今後の課題も浮き彫りになりました。ユーザーに価値を届けるというマインドの面でも学ぶことが多く、今後の仕事選びの指針になりそうです。エンジニアだけでなく、営業や雑誌編集をしている方などいろんな部署の人と机を並べて仕事ができたのはとても楽しく、かけがえのない体験でした! インターンからの内定 VASILYの新卒採用はインターンへの参加が必須としていますので、インターンはいわゆる内定直結型といえるでしょう。実際に、私は去年の夏のインターンを経て、VASILYに入社しました。 インターンでは、どっぷりiQONの開発に携わるので、マインドやスキルセットが合っているかを見極める良いチャンスです。内定者アルバイトの制度もありますので、入社後のミスマッチも少ないかと思います。 中途社員の経歴も、外資系コンサル、大手通信会社や大手ゲーム会社などなど様々なバックグラウンドのメンバーが揃っており、いろいろな立場の話が聞けます。私がベンチャー感の特に強いVASILYへの入社を決めたのも、社員の率直な意見をたくさん聞けて信頼できたからです。インターン参加中は、いろいろな部署の社員と話せるチャンスです。VASILYで働く人たちになんでも聞いてしまいましょう。 事前に知っておくといい知識 いまでは当たり前のように使っていますが、学生の頃には知らなかった便利なサービスやツールを紹介します。 仕事で触っているうちに使えるようになりますが、事前に動かせるようにしておくと良いスタートダッシュを切ることができるでしょう! GitHub ( https://github.com/ ) コードのバージョン管理をするサービスです。このサービスを使えば、書いたコードを公開したり、コードに変更が加えられた際、誰がどのように変更・追加・削除したのかを管理することができます。様々なコマンドがありますが、実際に触って覚えるのが良いでしょう。 Slack ( https://slack.com/ ) 情報共有ツールのひとつで、VASILYでは部署やプロジェクトごとにチャンネルを作り、チャットで情報を共有します。 ほかにも、アイディアを書き溜めておくチャンネルやBOTがニュースを集めてくるチャンネルなどさまざまなカスタマイズに対応できます。大学の研究室でも使っているところがあるようです。 BigQuery ( https://cloud.google.com/bigquery/what-is-bigquery ) Google Cloud Platformが提供するデータ分析ツールです。ギガバイト単位の大量のデータを圧倒的な速度で集計し抽出することができます。簡単な集計だけで済んでしまうような分析はBigQueryだけで完結させることもできます。最近、結果をそのままSpreadsheetに吐き出せるるようになり、ますます便利になりました。 VASILYではほぼすべてのデータがBigQueryに同期されており、データの結合や集計が素早くできるようになっています。 SQLの書き方を学んで必要な情報を抽出できるようになっておくと、データ分析の前処理がとても早くなります。 Google Cloud Dataproc ( https://cloud.google.com/dataproc/ ) データサイエンティストの業務にはコンピュータリソースが欠かせませんが、業務によって必要となるスペックが異なる場合が多いです。そんなとき、さまざまなスペックの分散クラスタを90秒で立ち上げ、分単位の課金で借りることができるDataprocが便利です。VASILYではスペックの高い分散クラスタを30分だけ起動させて、並列計算を一気に済ませてかかる料金を節約する、といった使い方もしています。 Tableau ( http://www.tableau.com/ja-jp ) コードを書かずにテンポよくデータを可視化することができる便利なツールです。BigQueryやGoogle Analyticsとの連携もでき、自動でデータを抽出して図を最新の状態に保つことができます。個人的に研究ではPythonのMatplotlibを使って作図していましたが、Tableauを使えば圧倒的に時間を節約できます。自動更新もしてくれるので、一度作ってしまえば、営業やマーケターが毎日データをとってきてExcelにコピペするという作業もなくすことができます。 アカデミック版 もあるので学生は気軽に使い始めることができます。 最後に 今回はVASILYのデータサイエンスインターンの紹介をしました。 VASILYではインターンを通年募集していますが、時間が取れる夏休みが絶好のチャンスです! 機械学習やプログラミングが得意な方、iQONが好きで開発してみたい方など、成長できる環境を用意していますので、是非VASILYのインターンに応募してみてください。 夏のインターンはデータサイエンスチームだけでなく、全部署で募集しています!
アバター
こんにちは、神崎( @tknzk )です。ElasticBeanstalk w/ multi-container Docker で構成しているad-serverのdocker image を alpine linuxベースのimageに置き換えました。 alpine linuxは、非常に軽量なdistributionで、DockerHubに登録されているmiddlewareなどの公式のdocker imageでも採用が進んでいるOSです。 http://www.alpinelinux.org/ 以前の ブログ にも書いたとおり、ad-serverは ElasticBeanstalkで管理された multi-containerなdockerでクラスタを組んで、アプリケーションを稼働させています。その構成は、下記のようになっています。 本体のアプリケーションがはいったContainer (ad-server) webのリクエストを受け付けるためのnginx logコレクタとしてのtd-agent 監視用のmackerel-agent とあるタイミングの docker imageのサイズは下記のようになっており、docker imageの肥大化がすすんでいました。 image size base os ad_server 924.6MB centos:6 nginx 134.1MB debian:jessie td-agent 448.4MB centos:6 mackerel-agent 423.1MB ubuntu:14.04 肥大化を抑制するための方針として、できるだけ軽量なOSをベースにすること、不必要なパッケージをインストールしないことやbuildするときにだけ必要なパッケージを適宜削除することとして、imageを作成することにしました。 nginx まずは、オフィシャルのimageが対応していたnginxをalpineベースのものに変更しました。 Dockerfileは下記のようになり、FROMとしてオフィシャルのalpineベースのものを指定しています。 FROM nginx:1.11.1-alpine MAINTAINER Takumi Kanzaki COPY nginx.conf /etc/nginx/nginx.conf ad-server ad-sereverはベースとなるruby, supervisord, mysqlをbuildしたimageに ad-server として必要なGemをinstallする imageをbuildするという構成になっていました。alpineをベースにするにあたり、下記のような調整を行いました。 前段のベースとなるdocker image ruby buildに必要なpackageはtemporaryとしてinstallしてuninstall supervisord Dockerfile FROM alpine:3.4 ENV HOME /root WORKDIR /tmp # skip installing gem documentation RUN mkdir -p /usr/local/etc \ && { \ echo 'install: --no-document'; \ echo 'update: --no-document'; \ } >> /usr/local/etc/gemrc # versions ENV RUBY_MAJOR 2.3 ENV RUBY_VERSION 2.3.1 ENV RUBY_DOWNLOAD_SHA256 b87c738cb2032bf4920fef8e3864dc5cf8eae9d89d8d523ce0236945c5797dcd ENV RUBYGEMS_VERSION 2.6.3 ENV BUNDLER_VERSION 1.12.5 # some of ruby's build scripts are written in ruby # we purge this later to make sure our final image uses what we just built RUN set -ex \ && apk add --no-cache --virtual .ruby-builddeps \ autoconf \ bison \ bzip2 \ bzip2-dev \ ca-certificates \ coreutils \ curl \ gcc \ gdbm-dev \ glib-dev \ libc-dev \ libffi-dev \ libxml2-dev \ libxslt-dev \ linux-headers \ make \ ncurses-dev \ openssl-dev \ procps \ # https://bugs.ruby-lang.org/issues/11869 and https://github.com/docker-library/ruby/issues/75 readline-dev \ ruby \ yaml-dev \ zlib-dev \ && curl -fSL -o ruby.tar.gz "http://cache.ruby-lang.org/pub/ruby/$RUBY_MAJOR/ruby-$RUBY_VERSION.tar.gz" \ && echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.gz" | sha256sum -c - \ && mkdir -p /usr/src \ && tar -xzf ruby.tar.gz -C /usr/src \ && mv "/usr/src/ruby-$RUBY_VERSION" /usr/src/ruby \ && rm ruby.tar.gz \ && cd /usr/src/ruby \ && { echo '#define ENABLE_PATH_CHECK 0'; echo; cat file.c; } > file.c.new && mv file.c.new file.c \ && autoconf \ # the configure script does not detect isnan/isinf as macros && ac_cv_func_isnan=yes ac_cv_func_isinf=yes \ ./configure --disable-install-doc \ && make -j"$(getconf _NPROCESSORS_ONLN)" \ && make install \ && runDeps="$( \ scanelf --needed --nobanner --recursive /usr/local \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u \ )" \ && apk add --virtual .ruby-rundeps $runDeps \ bzip2 \ ca-certificates \ curl \ libffi-dev \ openssl-dev \ yaml-dev \ procps \ zlib-dev \ && apk del .ruby-builddeps \ && gem update --system $RUBYGEMS_VERSION \ && rm -r /usr/src/ruby # SETUP pip supervisord RUN apk add --virtual .supervisord-deps --update \ python \ py-pip && \ pip install -q --upgrade "meld3==1.0.0" "supervisor" 2> /dev/null # SETUP bundler RUN gem install bundler --version "$BUNDLER_VERSION" # SETUP ssl certificatate file RUN ln -s /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem 後段のad-serverのアプリケーション用の docker image Gemのinstall native extension の build に必要なpackage を install/uninstall mysqlの必要ものだけ残して不要なバイナリは削除 Dockerfile #vim: set ft=ruby FROM quay.io/vasilyjp/ruby:2.3.1-alpine_3_4-build ENV LANG ja_JP.UTF-8 # --- SETUP: rubygems --- ADD Gemfile /tmp/Gemfile ADD Gemfile.lock /tmp/Gemfile.lock ENV GEM_HOME /tmp/ad_server/bundle # SETUP middleware deps # build-base : native exetension build # libgsasl : gem memcached # cyrus-sasl-dev : gem memcached # mariadb-dev : gem mysql2 # linux-headers : gem raindrops RUN apk add --virtual .middleware-deps --update \ mariadb-dev \ libgsasl \ cyrus-sasl-dev && \ apk add --virtual .gem-build-deps --update \ build-base \ linux-headers && \ cd /tmp && \ bundle install --clean --jobs=4 && \ apk del .gem-build-deps && \ rm /usr/lib/libmysqld* && \ rm /usr/bin/mysql* # すべての.bundle/configを無効化して、環境変数によって設定を反映させる ENV BUNDLE_IGNORE_CONFIG 1 ENV BUNDLE_GEMFILE /var/app/Gemfile ENV BUNDLE_DISABLE_SHARED_GEMS 1 ENV BUNDLE_JOBS 4 ENV BUNDLE_PATH /tmp/ad_server/bundle VOLUME /var/app WORKDIR /var/app EXPOSE 3000 CMD ["supervisord"] td-agent alpineをベースにすることを検討しましたが、td-agentのbuildが難しく断念しましたが、CentOS:7 にすることで、多少のimage sizeの削減ができました。 # vim: ft=Dockerfile FROM centos:7 ADD td.repo /etc/yum.repos.d/treasuredata.repo RUN rpm --import https://packages.treasuredata.com/GPG-KEY-td-agent && \ yum -q -y install --enablerepo=treasuredata td-agent && \ yum update -q -y \ nss-tools \ nss-util \ nss-softokn-freebl \ nss-softokn \ nss \ bind \ bind-libs \ bind-utils \ openldap \ libuser \ pam \ libssh2 \ libxml2 \ openssl \ sqlite && \ yum clean all CMD [ "td-agent", "-c", "/etc/td-agent/td-agent.conf", "--use-v1-config" ] mackerel-agent aplineベースでmackerel-agent, mackerel-agent-plugin, check-plugins をbuildするものを作成しました。 pull request を投げていますが、コメントにも書いてる通り、alpineのバグがあり一部のpluginが動かない状態です。 alpineで動かすのは厳しいことから、ubuntuベースで 不要なmackerel-agent-pluginを削除し、check-pluginは利用していないのでinstall自体をやめることにして、sizeの削減を行いました。 FROM ubuntu:14.04 # setup mackerel-agent RUN apt-get update \ && apt-get -y install curl sudo ruby docker.io \ && curl -fsSL https://mackerel.io/assets/files/scripts/setup-apt.sh | sh \ && apt-get update \ && apt-get -y install mackerel-agent mackerel-agent-plugins \ && apt-get clean \ && rm -rf /usr/bin/mackerel-plugin-apache2 \ && rm -rf /usr/bin/mackerel-plugin-conntrack \ && rm -rf /usr/bin/mackerel-plugin-elasticsearch \ && rm -rf /usr/bin/mackerel-plugin-gostats \ && rm -rf /usr/bin/mackerel-plugin-haproxy \ && rm -rf /usr/bin/mackerel-plugin-jmx-jolokia \ && rm -rf /usr/bin/mackerel-plugin-jvm \ && rm -rf /usr/bin/mackerel-plugin-mailq \ && rm -rf /usr/bin/mackerel-plugin-munin \ && rm -rf /usr/bin/mackerel-plugin-php-apc \ && rm -rf /usr/bin/mackerel-plugin-php-opcache \ && rm -rf /usr/bin/mackerel-plugin-plack \ && rm -rf /usr/bin/mackerel-plugin-postgres \ && rm -rf /usr/bin/mackerel-plugin-rabbitmq \ && rm -rf /usr/bin/mackerel-plugin-snmp \ && rm -rf /usr/bin/mackerel-plugin-squid \ && rm -rf /usr/bin/mackerel-plugin-td-table-count \ && rm -rf /usr/bin/mackerel-plugin-trafficserver \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ADD startup.sh /startup.sh RUN chmod 755 /startup.sh # boot mackerel-agent CMD ["/startup.sh"] 現在のproduction環境の docker images 上記のように、ベースのOSを変更したり、 Dockerfileを工夫したりをして、docker imageのsizeを削減することができました。現在のproduction環境で動かしているimageの一覧は下記の通りです。 image size base os ad_server 342.7MB alpine:3.4 nginx 59.63MB alpine:3.4 td-agent 430.8MB centos:7 mackerel-agent 357.5MB ubuntu:14.04 [ec2-user@ip-xx-xx-xx-xxx ~]$ sudo docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE quay.io/vasilyjp/mackerel-agent 0.31.1-ubuntu-14_04_20160617 32eca0f192e6 4 days ago 357.5 MB quay.io/vasilyjp/ad_server 9c2aa373ff9f9c28aa162207b1c4511eb2dacf47 4d76a297c1d0 4 days ago 342.7 MB quay.io/vasilyjp/nginx 1.11.1-alpine_3_4-build 7a4a5e149521 8 days ago 59.63 MB quay.io/vasilyjp/td-agent 0.12.20-centos7 fa60e55fb621 4 weeks ago 430.8 MB amazon/amazon-ecs-agent latest 46e05d110968 5 months ago 9.097 MB まとめ すべてのdocker imageが450MB以下になり、トータルでは既存の6割程度のサイズに落とすことができました。 本当に必要なものだけを指定して構築することで、不要な物がなくなり、セキュリティ的にも安心できる構成が取れたかと思います。 mackerel-agentのところでも触れたように、alpineは少しbuggyなところもありますが、4月末からad-serverのproduction環境に投入し、先日3.4系への移行も行いましたが特に問題なく稼働できています。 最後に VASILYでは、一緒に開発をしてくれる仲間を募集しています。 Dockerを使った開発/運用をしてみたい方は以下のリンクをご確認ください!
アバター
こんにちは、エンジニアの堀江( @Horie1024 )です。先日行われた Android Testing Bootcamp #2 で「AndroidのCI環境をCircleCIからWerckerにした話」という内容で発表させて頂きました。発表に使用したスライドはこちらになります。 この投稿では、スライドでは単にリンクを貼って終わらせてしまったなど、詳細を紹介しきれなかった点についてご紹介しようと思います。 移行前に利用していたCircleCIによるCI環境について スライドでも紹介しましたが、iQONの開発では、1年半ほど前からCircleCIを導入していました。導入についての詳細は以下の投稿にまとめてあります。 tech.vasily.jp CircleCIで行っていたことは以下の通りです。 ユニットテスト BetaでのAPK配布 Google Playへのアップロード自動化 図にすると以下のようになります。 ユニットテスト ユニットテストは Robolectric を使いJVM上でのテストを実行しています。使用しているテスティングフレムワークは JUnit 、モックライブラリは mockito です。現状カバレッジは高く無く、モデル部分相当するクラスについて必要に応じて書いています。 UIテスト AWS DeviceFarm を利用して Calabash で書いたテストを実行していました。 tech.vasily.jp Calabashを選択した理由は、CucumberがサポートされGherkinでfeature(テストコード)を書くことができるのが理由です。以下は、ログイン後に画面を目的のコンテンツ位置までスクロールし、ログアウトするfeatureです。自然言語に近い文法で書くことができるため理解しやすくなっています。 ただ、コスト的な問題と Cloud Test Lab(現Firebase Test Lab) がCalabashでのテストの実行をサポートして無いことから、現在では Espresso を利用し実機を使用してローカルでテストを実行しています。 Espresso Test Recorder がGoogle I/O 2016で発表されていますし、EspressoでのUIテストがより行いやすくなるのを期待しています。 APK配布 APKの配布について当初はDeployGateを利用していましたが、Crashlytics(現Fabric)を利用していたこと、iOSがBetaを採用したこともあり、Betaに変更しています。 Google Playへのアップロード自動化 Google Playへのアップロード自動化については、当初PythonのGoogle APIs Client Libraryを使用する予定でした。 qiita.com CircleCIでPythonのGoogle APIs Client Libraryを使用するには、各種ライブラリのインストールや依存関係の解決などを行わなければならず、より簡単な方法を探していたところ gradle-play-publisher プラグインを発見し、現在でも利用しています。以下のスライドでは、CircleCIでののgradle-play-publisherプラグインの利用方法について言及しています。 Androidのビルド用Dockerイメージの作成 Werckerを利用するには、Dockerイメージが必要になります。 Androidプロジェクトをビルド可能なイメージはDockerHubなどで公開されていますが、 Android SDKをDockerイメージに含めてpublicで公開すると 再配布 としてライセンス違反となることと、CIサービス側とローカル環境とのビルド環境のズレを制御するためにイメージを自作しプライベートリポジトリとしてホストしています。SDK VersionやBuild Tools Versionを変更する度Dockerfileを更新する必要がありますが、DockerHubやQuay.ioにはGitHubへのpushをhookしてイメージのビルドを自動的に行うAutomated Buildという機能が用意されているのでメンテナンスコストはほぼ掛かっていません。 qiita.com ビルドしたイメージは弊社のADサーバーで利用するDockerイメージと同様に Quay.io にプライベートリポジトリとしてホストしており、プライベートリポジトリの場合wercker.ymlでのboxセクションの書き方が若干変わります。 boxセクションでプライベートリポジトリを指定する方法は、 こちらのドキュメント が参考になります。そして、yamlは以下のようになり、idにはリポジトリ名、username、passwordについては、WerckerのWeb UIで入力した環境変数を参照するようにします。 box : id : quay.io/knuth/golang username : $USERNAME password : $PASSWORD tag : beta registry : quay.io build : steps : - script : name : echo code : echo "hello world!" また、DockerHubのPrivate Repositoryの場合以下のようなyamlになります。DockerHubの場合registryの指定は不要です。 build : box : id : guido/python username : $USERNAME password : $PASSWORD tag : latest steps : - script : name : echo "hello world!" より詳しい内容は以下の記事にまとめてあります。 qiita.com keystoreや.p12キーファイルの扱い AndroidアプリのCI/CDをどう行うのかを考えるとkeystoreやkey.p12などのファイルを何処に置くか?が問題になることがあります。今回、keystoreなどのファイルをIP制限をかけたs3のバケットに置き、それをWerckerから取得する方法で解決しました。Werckerが使用するIPアドレスをWhitelistとしてs3バケットのバケットポリシーに追加することで、Werckerからのアクセスのみに制限できます。WerckerのソースIDリストは こちら から確認できますが、予告無しに変更される可能性があるため注意が必要です。 qiita.com Wercker cache Werckerでは、Workflowsで繋いだpipeline間で共有出来るディレクトリがあり、キャッシュとして利用できます。キャッシュディレクトリのパスは、環境変数 WERCKER_CACHE_DIR を参照することで取得できます。Wercker cacheの詳細は以下のドキュメントをご覧ください。 http://devcenter.wercker.com/docs/pipelines/wercker-cache.html ビルド速度の改善 WERCKER_CACHE_DIR をGradleのキャッシュディレクトリとして利用することでビルド速度を改善できます。Gradleは実行時に --project-cache-dir を用いることで任意のキャッシュディレクトリを指定できます。詳細は 付録D Gradle コマンドライン をご覧ください。したがって、以下のように指定することで WERCKER_CACHE_DIR をキャッシュディレクトリとして利用できます。 $ ./gradlew --project-cache-dir=$WERCKER_CACHE_DIR testDebug Gradle Wrapperを利用する場合 Gradle Wrapperを利用する場合は、Werckerの環境変数として GRADLE_USER_HOME を定義し $WERCKER_CACHE_DIR を指定します。これでWrapperがダウンロードしたGradleを異なるpipeline間で共有でき、pipeline毎にGradleがダウンロードされることが無くなります。 環境変数は「Settings」の「Environment variables」から定義できます。 WERCKER_CACHE_DIRの有効期限 各pipelineは、スタート時にWERCKER_CACHE_DIRにキャッシュをロードします。ロードされるキャッシュは、14日間以内で最後に成功したビルドのもので、キャッシュの容量は1GBまでとなっています。 キャッシュの消去 キャッシュを消去したい場合、「Settings」の「Options」にある「Clear cache」から削除できます。また、Gradle実行時に --recompile-scripts を付けることでキャッシュが全て破棄され再度コンパイル、保存されます。詳細は 「キャッシング」 の項目をご覧ください。 まとめ 今回Android Testing Bootcampに参加してみて、様々な取り組みや事例、tipsについて知ることができ非常に勉強になりました。また、発表する機会を頂いたことで自分自身の知識を整理することができ、CI/CDを含めた今後のiQONの開発フローをどのようにしていくのかのヒントを多く得られました。次回のAndroid Testing Bootcampへも是非参加できればと思います。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 また、VASILYでは今年もエンジニア向けインターンシップを行います。Androidチームでのインターンシップについては、以下の記事で紹介していますので、ご興味のあるかたは是非ご覧ください。 tech.vasily.jp
アバター
Androidエンジニアのnissiyです。学生のみなさん!インターンシップに参加していますか? 近年インターンシップに参加する学生が増えているそうですが、VASILYでも2014年からエンジニア向けインターンシップのプログラムを組んで学生を受け入れています。 募集は通年行っていますが、まとまった時間が取れる夏休みを利用して参加される方が多い傾向にあります。 今回は、昨年のAndroidチームを例にVASILYでのインターンシップの紹介をしたいと思います。 VASILYのインターンシップの特徴 VASILYのエンジニア向けインターンシップでは、すべてのチーム例外なく社員と一緒にiQONに関わる開発を行ってもらいます。 インターンシップ期間中に書いたコードは本番に組み込むため、チーム内でしっかりとコードレビューを行います。 よくあるハッカソン的にプロダクトを作って発表するタイプのインターンシップではないため、本当の意味で就業体験を行うことができます。 インターンシップ参加の条件 インターンシップの参加条件としては、こちらが提示する課題をクリアしてもらう必要があります。 実際にiQONを開発していただくということで、少しハードルの高い課題を設定しています。 ちなみに、昨年のAndroidチームのインターンシップ受け入れ課題は以下のような内容になっていました。 InstagramのAPIを使って『iQON』のタグが付いた画像をグリッド表示するアプリを作成し、 Githubにソースコードを公開して、URLを共有すること 【ルール】 ・Android Studioを使って開発すること ・Libraryは自由に使ってOKとする ・グリッド表示にはRecyclerViewを使って実装すること ・ActivityとFragmentは自由に使って構わないがActivityは、AppCompatActivityを継承すること ・Toolbar(ActionBar)を実装すること ・スクロールによってToolbarを隠すか、隠さないかは自由とする 課題の提出後は、チーム内でコードレビューをしたあとにフィードバックを行います。 何度か修正のやりとりを経て、課題クリアか判断します。 課題クリアのポイントはいくつかありますが、以下の項目は重視して見ていました。 Web APIを使った開発ができるか Androidアプリの基本的なお作法を理解しているか コミュニケーションを取った開発ができるか GitやGithubの基本的な使い方を理解しているか 課題遂行において知らない知識があっても自学できるか *提出された課題アプリのキャプチャ インターンシップで取り組んでもらった内容 課題をクリアした方は、VASILYでの楽しいインターンシップ生活が待っています。 昨年のAndroidチームでは、主に以下のような内容に取り組んでもらいました。 SNS連携の改善&グロースハック オンボーディングの改善&グロースハック オンボーディングで使用する特殊なViewの開発 Material Design対応 iQONで利用しているOSSへのコミット など... 希望によって、実装メインかグロースハックメインかで大きく分かれますが、どちらを選んでも難易度が高く面白い内容になっています。 その他、一般的なインターンシップと同様に、CEOからの会社説明があったり、最終日に取締役への成果報告会などがあったりします。 *Pull Requestを送ると暖かいコメントが数多くもらえます VASILYのインターンシップ参加者の声 東京大学Kくん VASILYのインターンに参加したおかげで、Gitなどのツールの使い方を学ぶことができ、自分の開発力が3倍くらいになったと思っています。 Googleにも認められているAndroidアプリのソースコードを見ながら開発できるのは大変勉強になりました!ありがとうございました! 法政大学大学院Kくん 技術的な面はもちろん、サービスやプロダクトに対するマインドといった面も勉強させて頂きました。 大学を卒業し『エンジニアとして生きていく』ということがどういうことなのかを肌で体験できるインターンシップでした。 インターンシップからの内定 他社のインターンシップでは新卒採用の選考とは全く関係ないものもあると思いますが、VASILYでは新卒採用の選考フローにインターンシップへの参加が入っているため、新卒入社するためにはインターンシップへの参加が必須になっています。 インターンシップ参加後に、インターンシップ期間中の成果や、会社とのマッチングを見て、その後の選考を受けるか決めてもらうようにしています。 最後に 簡単ではありますが、昨年のAndroidチームを例にVASILYのエンジニア向けインターンシップを紹介しました。 多くのユーザーが利用しているアプリの開発に携われるため、刺激的な就業体験になること間違いなしです。 チームを開発してみたい方、プログラミングが大好きな方、iQONを作ってみたい方、ぜひともVASILYのインターンシップに応募してみてください。 Androidチームだけでなく、すべてのエンジニアチームで募集中なので、詳しくは下のリンク先で確認してください。
アバター
こんにちは、VASILYバックエンドエンジニアの塩崎です。 社会人2年目にも突入し、優秀な後輩たちに抜かされないかと日々ひやひやしています。 さて、今回は1ヶ月程前に完了した、メールサーバーのSendGrid移行について紹介したいと思います。 移行のきっかけ そもそも、なぜVASILYでメール配信の自社管理をやめてクラウドサービスであるSendGridに移行する必要がでたのでしょうか? 以前から使用していたpostfixサーバーではなぜダメだったのでしょうか? それは、大量のメールマガジンを遅延なく配信する必要が生じたからです。 昨年の11月頃からiQONでは、ユーザーさん一人一人にオススメのアイテムを送る、リコメンドメルマガを開始しました。 それにあたり、数万人のユーザーさんに対してそれぞれ内容の少しずつ異なるメールを送る必要が出ました。 このような処理を自社管理のpostfixサーバーで行うことは 非常に面倒臭い 非現実的であったため、面倒な処理を肩代わりしてくれるクラウドサービスを利用することにしました。 SendGridに決めた理由 メール配信のクラウドサービスを決めるにあたって、SendGrid以外にも Amazon SES 、 Mandrill 、 MailChimp などのサービスも比較しました。 それらと比較してSendGridが優れているポイントを紹介します。 ただメールを送るだけのサービスではない SendGridはメールを送るだけのサービスではありません。メール配信に関わる機能一式を提供してくれます。 これが、ただメールを送るだけのサービスであるAmazon SESとの大きな違いです。 個人間で送り合うメールならいざ知らず、メールマガジンのような大量のメールを一斉配信することは想像している以上に困難です。 少しでも怪しいメールを送ると携帯キャリアなどのメール受信業者のブラックリストに入ってしまいます。 そのような扱いを受けないようにするためにSPFやDKIMを適切に設定する必要があります。 SendGridはこれらの認証機能を簡単に使用することができたので導入が非常に楽でした。 また、ユーザーさんのメールボックスに届いた後にユーザーさんがどのような行動をするのかも非常に重要です。 メールを開封するのかどうか、メール中のリンクをクリックするのかどうか、これらの指標はメールマーケティングを行うためには最も重要な指標です。 しかし、これらの機能を自社で作り上げるのは 非常にメンドイ そこそこの人月が必要です。 そのような機能をデフォルトで提供してくれて、しかも綺麗なダッシュボードにまとめてくれるのも嬉しいポイントです。 WEB APIが豊富 SendGridが提供している、ほとんどの機能をWEB APIから使うことができます。 SMTPという旧世紀のプロトコルを使うことなく、近代的な方法でメールの送信を行うことができます。 さらに送信をするためのWebAPIだけでなく、何かイベントが起こった時に特定のエントリーポイントを叩いてくれる機能もあります。 メールの到着、開封、クリックなどのイベントが起こったタイミング毎に指定したエントリーポイントを叩いてくれます。 VASILYではこの機能を利用して、メール配信のログをBigQueryに保存し、それをBIツールであるTableauで可視化しています。 このあたりについては記事の後ろのあたりで、より詳しく紹介します。 日本代理店がある SendGridの日本代理店として 構造計画研究所 さんがあり、いざというときに日本語のサポートがあるのが安心でした。 また、構造計画研究所さんではSendGridの初心者向けセミナーや、個別相談説明会、メールマーケティングに関する ブログ などの活動を精力的に行っており、ただ本国に取り継ぐだけでなく、メール配信のプロがきちんと日本にもいるという実感が持てました。 注) 色々とSendGrid万歳な紹介をしていますが、弊社はSendGridの回し者ではありません。 使い方によっては他のメール配信サービスを使ったほうがいい場合もあります。 例えば、対エンジニア向けのアラートメールなどは、アプリケーションサーバーに同居しているsendmailで送信を行っています。 移行するためにしたこと アドレスリストのクリーニング 移行の本来の目的はメルマガ配信の効率化であったため、保持しているメールアドレスリストが大量配信に耐えられるものかどうかを考える必要があります。 諸所の事情により、当時のアドレスリストはダブルオプトインが確認できているかが不明だったために、バウンス率が非常に高くなってしまう恐れがありました。 そのため、SendGridに移行を行う前にアドレスリストのクリーニングを行いました。 幸いにも、シングルオプトインは当時のログから確認できたため、配信対象のユーザーに対してメルマガを試しに送信してみて、開封ログを取ることにしました。 この時はまだSendGridへの移行をしていなかったので、開封トラッキングの仕組み・開封をしてくれたユーザーさんのダブルオプトイン確認フラグを立てる仕組みを自力で作りました。 車輪の再発明をしている感じしかなく、面倒くさかったです。 数回メルマガを送っても開封してくれないユーザーさんについては、潔く諦めました。 エンゲージメントの低いユーザーさんに送ってもコストの方が嵩むということに加え、ハニーポットに引っかかるのを防止する効果も期待できます。 また、バウンス率の高いメールを一気に送ることによるレピュテーションの急激な低下を防ぐために、メールを「ゆっくり」送るように、スケジューラーを設定しました。 メールアドレスリストを、主要メール受信業社数社分のリストに分け、それぞれのリストに対して一定のペースで送るようなプログラムを自力で作りました。めんどうくさかったです。 さらに、「レピュテーションは資産である」ということを常に念頭に置き、レピュテーションやブラックリストを確認する外部サービスの結果を毎朝確認し続けました。 レピュテーションを確認できるサービスについてはこちらの構造計画研究所さんのBlogが詳しくまとまっていたので参考にしました。 送信レピュテーションを確認する5つの方法 この辺りの作業については、先日のSendGrid Night #4でもLTを行いました。 メールアドレスの文字コード SendGrid移行前は mail-iso-2022-jp を利用して、ISO-2022-JP(俗に言うJISコード)でメールの配信を行っていました。 ですが、その設定のままで送信を行うとメールの文字化けが発生してしまったため、このgemを取り除き、UTF8でメールの送信を行うようにしました。 ActionMailerの設定の修正 以下の記事を参考にして、ActionMailerが指し示すサーバーを自社管理のpostfixサーバーから、SendGridのSMTPサーバーに変更しました。 https://sendgrid.kke.co.jp/docs/Integrate/Frameworks/rubyonrails.html#Configure-ActionMailer-to-Use-SendGrid 移行結果 以上の作業を行った結果、1ヶ月程度前にメールサーバーのSendGrid移行が完全に完了しました。 作業が面倒くさそうな書き方をしていますが、ただ置き換えるだけであればActionMailerの設定を書き換えるだけですので非常にシンプルです。 ダブルオプトイン確認フラグの復旧が面倒だっただけです。 VASILYでは社内KPIをBigQueryで集計、BIツールであるTableauで可視化を行っているので、メルマガの配信結果もそれらを利用して作ってみました。 以下に示すような仕組みでSendGridのEventWebHookから飛んでくるデータを可視化しています。 その結果、このようなダッシュボードをエンジニアだけでなく、ビジネス職の人とも共有し、効果的に施策を考えることができています。 ここは改善してほしいなSendGrid さて、ここまでSendGridを褒めちぎってきましたが、一部使いづらいと思うこともあったので紹介します。 直してくれないかなぁ・・・ 予約配信した時に時刻がズレる → 解決しました SendGridには予約配信機能があり、送信リクエストをしたタイミングと、実際に送信が行われるタイミングをずらすことができます。 メルマガではこの機能を利用し大量のメールを遅延なく配信しています。 しかし、ユーザーさんの手元に届いた時のメールのタイムスタンプが、届いたタイミングではなく送信予約をしたタイミングになってしまいます。 例えば、以下のメールはAM 11:00頃に配信予約をし、PM 9:00にユーザーさんの手元に届いたメールです。 メール中のタイムスタンプが配信予約のリクエストを投げた時刻になってしまっています。 この問題は先日のSendGrid Night #4でSr. Product Support EngineerのScott Kawai氏に質問したところによると、配信予約時にSMTPヘッダーのDateフィールドをPM 9:00に設定すれば良いということを教えていただきました。ありがとうございます。 再送するタイミング SendGridはsoft bounceしたメールを最大で72時間再送しようと試みてくれます。 一方でパスワードリセットや、領収書などのトランザクションメールはどんな時間に届いても価値があるかと思います。 しかし、メルマガのようなマーケティングメールが深夜に届いたらユーザーさんはどのように思うでしょうか? 深夜3時に届いたメーケティングメールで目が覚めてしまうなんて、酷い目覚めではないのかと思います。 そのため、メールによっては日中のみに再送をするような設定ができたらいいなと思います。 まとめ SendGridを実際に使ってみて、SendGridはメールを届ける「だけ」のサービスではないということがわかりました。 メールを届けるのはもちろんのこと、その他メールに関わる色々な面倒臭い処理をまとめて引き受けてくれるサービスであるということがわかりました。 メールをただしくユーザーさんのメールボックスに届けるのが予想外に難しいことを考えると、ほとんどの場合これらの面倒くさい処理をSendGridに任せた方が得策なのではないでしょうか。 終わりに VASILYではモダンなインフラに興味のある仲間を募集しています。 興味のある方は是非こちらからご応募ください。
アバター
こんにちは。iQONのバックエンドエンジニアを担当しているjoeと申します。 最近、iQONのお知らせ機能のDBをMySQLからDynamoDBへ移行しました。 移行する際に発生した問題点である並列処理によるデータ欠損とProvisioning超過の対策を書きます。 間違っているところや改善点があればご指摘ください。よろしくお願い致します。 お知らせ機能とは お知らせ機能とは、facebookで言うところの「◯◯さんがあなたの投稿に「いいね!」といっています」のような、ユーザーに対するアクションがあったことを通知したり、アイテムが値引きされた、アイテムの在庫が少なくなった等のlikeしたアイテム情報をユーザーに通知する機能です。 既存のお知らせ機能の問題点 既存の構成における問題点は以下の二点です。 データの肥大化 レスポンスが遅い お知らせのデータ構造は、下記のようになっています。 MySQL : マスターデータ redis : キャッシュ 図 お知らせのデータ保持の構成図 redisにはユーザーごとのMySQLのお知らせidを直近100件分保持しています。 お知らせはユーザーの行動やアイテムのステータスの変化の分レコード数が存在するので、MySQLのレコード数が尋常ではない量になります。また、ユーザー分のindexをredisに保持しているので、これではユーザーの人数が増えれば増えるほどredisの容量を食う事になります。MySQLのデータが大きくなればなるほどレスポンスが遅くなっていきます。 この問題を解決するために、awsの DynamoDB に移行することにしました。 なぜDynamoDB? DynamoDBは スキーマレスなkey, valueストレージ 10ms未満のレイテンシー 大規模なデータにも柔軟に対応 自動で冗長化してくれるのでメンテが楽 のような特徴があり、今回解決したい問題の2つである速度改善とデータの肥大化への対策が期待できます。 また、DynamoDBのデメリットである柔軟な検索や集計処理が不得意という点に関しては、今回のケースで影響は少ないと考えられます。 DynamoDBでのデータ保持 ユーザーごとにkeyを持たせて、お知らせはJSONを要素とする配列を格納するようにしました。元々redisで100件分しかデータを保持していなかったので、配列の長さは100件までとしました。 update_countという項の存在意義は後ほど説明します。 event_typeというのは発生したeventごとに割り振られているユニークな数値です。これによってお知らせの種類を見分けています。 # ユーザー1人分のお知らせデータ feedback_user_id(primary_key): "feedback:user:40" feedbacks: [ {"event_type":11,"set_id":1079944,"create_time":"2016-04-30 12:43:56 +0900","feedback_id":500000566}, {"event_type":11,"set_id":1080144,"create_time":"2016-04-30 12:43:58 +0900","feedback_id":500000568}, {"event_type":11,"set_id":1079456,"create_time":"2016-04-30 12:44:00 +0900","feedback_id":500000570}, {"event_type":11,"set_id":1073230,"create_time":"2016-04-30 12:44:01 +0900","feedback_id":500000572}, {"event_type":11,"set_id":1068334,"create_time":"2016-04-30 12:44:03 +0900","feedback_id":500000574}, {"event_type":11,"set_id":1064505,"create_time":"2016-04-30 12:44:05 +0900","feedback_id":500000576}, {"event_type":11,"set_id":1065469,"create_time":"2016-04-30 12:44:06 +0900","feedback_id":500000578}, {"event_type":11,"set_id":1055427,"create_time":"2016-04-30 12:44:07 +0900","feedback_id":500000580} ], update_count: 766 お知らせ機能のdynamo移行における問題点 お知らせ機能のdynamo移行における問題点は2つです。 provisioning量超過 並列処理実行によるデータの欠損 お知らせの発行において予測できない書き込みの増加が発生することがあるので、provisioning量超過時のエラーの対処をする必要がありました。 また、iQONのお知らせデータの発行はworkerによる遅延処理のため、複数のworkerが同時にdbにアクセスしてデータを書き換えるという事例が発生し、データが欠損する恐れがありました。 それぞれの問題についての今回の対応を書いていきます。 provisioning量の調節 いきなりですが、provisioning量を自動調節する方法は弊社の別のブログで公開しているのでそちらを参照してください! DynamoDBの導入とDynamic DynamoDBを用いたプロビジョニング量自動調整 ただ、上記の場合だと急激なthroughput(読み込み・書き込み量)の変化に追いつけずに書き込みに失敗することがあるので、今回はprovisioningを超過して失敗した場合、お知らせの発行処理をenqueueしてworkerに処理させるようにしました。 実装例 お知らせをDynamoDBにinsertする処理をwith_retryメソッドで囲み、insertが失敗した場合にworkerにenqueueしています。 # retry処理 def with_retry begin yield rescue Aws :: DynamoDB :: Errors :: ProvisionedThroughputExceededException , Aws :: DynamoDB :: Errors :: ConditionalCheckFailedException => e if get_event_value[ :retry_enqueue_limit ] != 0 @retry_enqueue_limit -= 1 Sidekiq :: Client .enqueue( FeedbackDynamoFailedInsertWorker , get_event_value) else :: Rails .logger.info " [FEEDBACK ENQUEUE RETRY MAX] #{ e.message }" raise e end end end # 失敗のqueueを処理するworker class FeedbackDynamoFailedInsertWorker < Worker sidekiq_options :queue => :feedback_failed_insert , :retry => false , :backtrace => true def perform (params) sleep( 0.005 ) begin FeedbackUserIndexDynamo .new(params[ " user_id " ]).update_feedback!(params[ " event_value " ], params[ " retry_enqueue_limit " ]) rescue => e :: Rails .logger.info " [ERROR][FeedbackDynamoInsertWorker] " + e.message end end end 上記を実装することで完全にthroughputによるエラーをなくせました! 並列処理実行によるデータの欠損 iQONのお知らせデータの発行はworkerによる遅延処理のため、複数のworkerが同時にdbにアクセスしてデータを書き換えるという事例が発生します。そこで起こりえるのが、同時データ取得・書き込みによるデータの欠損です。 例えばworker1がDynamoDBにアクセスしてお知らせのデータを取得し、お知らせの配列にデータを追加してupdateをする。その間にworker2がDynamoDBにアクセスして値を追加、worker1の動作を上書いてしまうといった不整合が発生します。 そこで、 DynamoDBの機能としてある"Conditional Update(条件付き書き込み)"という楽観的ロックを用いてデータの欠損を防ぎました。 "Conditional Update"とは、DynamoDBのupdate処理のoptionの一つであり、こちら側が指定した条件を満たす時のみデータをupdateするようにする処理です。 実際の構成 Conditional Updateの条件にするため、上記でも説明したupdate_countというcounterをお知らせの更新のたびにincrementします。 Conditional Updateの条件を下記のように設定して同時書き込みを防ぎました。 条件:  update_count == previous_update_count 図 お知らせ更新の構成図 ※ iQONのお知らせは、過去のお知らせ情報を元に丸め処理(同じコーデに対するLikeのお知らせは1個に丸める等)をするため、DynamoDBからお知らせの配列ごと取得し、お知らせを書き換えて配列をDynamoDBに上書きするというフローになっています。 実装例 insert def add! (feedbacks, update_count, previous_update_count) options = { table_name : " feedbacks " , key : " feedback:user:1 " , update_expression : " SET update_count = :update_count, feedback = :feedbacks " , condition_expression : " update_count <= :previous_update_count " , expression_attribute_values : { " :feedbacks " => feedbacks, " :update_count " => update_count, " :previous_update_count " => previous_update_count } } @@dynamodb .update_item(options) end update処理を行うときは、 update_item を使用しますが、 update_item のオプションは AttributeUpdates でなく、 UpdateExpression を使用することが推奨されています。 また、update_expressionでは下記の4つのupdateのオプションがあります。 SET REMOVE ADD DELETE 今回は ADD より柔軟な SET を使用しています。(ADDは数値とset型の配列しか受け付けない) また、 SET を使えば、数値をincrement/decrementすることもできるので、update_countのincrementをDynamoDB側で行う事もできます。詳しくは参考資料を御覧ください。 (今回は自前でincrementしています。) update_expressionの参考資料 Modifying Items and Attributes with Update Expressions その他の注意点としては、 数値の型がintではなくBigDecimal Time型がないので時間は文字列で値を入れる 等があります。 Conditional Updateの失敗数の監視と失敗時の対応 Conditional Updateのfail、つまり、同時書き込みによる失敗数は下記の図のようになっています。 このように、DynamoDBではthroughtputの量やConditional Updateのfail等、awsコンソールで表示してくれます。 Conditional Updateの失敗をした場合は、上記で説明した用にworkerでenqueueして処理を再度行うようにしています。 結論 各対処法によってデータ欠損、Provisioning量超過によるデータの書き込みエラーで飛んでくるsentryのエラーを0にできました! DynamoDBを触ってみて、配列の上限がx個に達したら自動的に古い要素を削除してくれるような機能があればいいなと思いました。 このような機能がある、又はDynamoDBの◯◯を使えばコレが実現できそう等がありましたら教えて下さい! 終わりに VASILYではDynamoDBをごりごりに使ってみたい仲間を募集しています。 新規事業の開発も始まりましたし、ご興味がある方はぜひこちらからご応募ください。
アバター
iOSアプリを開発しているエンジニアの庄司です。 今回は、iPhoneでのテザリング中や通話中に、ステータスバーの高さが変わることによる表示崩れの対応について紹介します。 TL;DR iPhoneでテザリング中、 UITabBar が画面からはみ出したりすることへの対応方法です。 RootViewControllerのviewに UITabBarController のviewを addSubview: するときは、親viewの中に収まるようにAutoLayoutを設定します。 scrollView.contentInset の調整には topLayoutGuide.top を使います。 サンプルアプリをGitHubにあげています。[ GitHub ] 何が起きていたか テザリング中や通話中などにレイアウトが崩れる UITabBar が20pts下がって、画面からはみ出しまう ViewController構成
 UIViewController // RootViewController |- UITabBarController |- UINavigationController | |- UITableViewController |- UINavigationController |- UITableViewController RootViewController内の viewDidLoad で UITabBarController をコードで追加しています UITabBarController をRootViewControllerとするXcode Projectでは、この問題は発生しません Viewデバッガで見てみる 下記のような位置関係になっているため、 UITabBar がはみ出して見えます。 
 // UIWindowからの相対的なframe UIWindow : ( 0 , 0 , 375 , 667 ) RootViewController : ( 0 , 20 , 375 , 647 ) UITabBarController : ( 0 , 40 , 375 , 647 ) UITabBar がはみ出してしまう問題の対応 UITabBarController のviewを addSubview した後、AutoLayoutを設定して UITabBarController のviewがsuperviewの中に収まるようにします Before // RootViewController.swift override func viewDidLoad () { super .viewDidLoad() let tc : UITabBarController = createTabBarController() addChildViewController(tc) view.addSubview(tc.view) tc.didMoveToParentViewController( self ) } After // RootViewController.swift override func viewDidLoad () { super .viewDidLoad() let tc : UITabBarController = createTabBarController() addChildViewController(tc) view.addSubview(tc.view) // view の中に収まるように、tabBarController.view に constraintを設定 view.addFittingConstraintsFor(tc.view) tabBarController.didMoveToParentViewController( self ) } extension UIView { /** childViewが同じサイズに収まるように、constraintsを設定する - parameter childView : 子View */ func addFittingConstraintsFor (childView : UIView ) { let constraints = [.Top, .Leading, .Bottom, .Trailing].map { NSLayoutConstraint( item : childView , attribute : $0 , relatedBy : .Equal, toItem : self , attribute : $0 , multiplier : 1.0 , constant : 0.0 ) } childView.translatesAutoresizingMaskIntoConstraints = false addConstraints(constraints) } } 修正結果 UITabBarController のviewはRootViewControllerのviewと同じ位置、サイズになりました。 コンテンツ開始位置のズレ このサンプルでは特に問題はありませんが、テザリング中に UITableView のコンテンツ開始位置がズレる現象もよく見かけます。 ステータスバーのサイズ UIApplication の statusBarFrame が変わります。 テザリング中は見た目通り、高さが40で返ってきます。 // 通常時 statusBarFrame : ( 0 , 0 , 375 , 20 ) // テザリング中 statusBarFrame : ( 0 , 0 , 375 , 40 ) しかし、上記のViewデバッガで見てわかるように、RootViewControllerが 20 だけ下がります。 
下記のようなコードを書くと、通常時と比べてコンテンツ開始位置が 20 だけ下がって見えてしまうでしょう。 // 通常時: 20 / テザリング時: 40 scrollView.contentInset.top = statusBarFrame.height topLayoutGuide を使う UIViewController の topLayoutGuide はテザリング中でも値が変わりません。 topLayotGuide.top はステータスバーやナビゲーションバーの高さも考慮した値を返します。 ランドスケープ時にステータスバーが消えた場合は、ナビゲーションバーの高さだけ返してくれます。 // 通常時: 20 / テザリング時: 20 scrollView.contentInset.top = topLayoutGuide.top 所感 特に情報が見つからなかったので、独自の解決策です。 もっと良い方法や、Appleの公式なドキュメントがあれば教えて下さい。 UITabBarController をRootViewControllerとしてStoryboardで実装した場合は、今回の問題は発生しませんでした。 国内外・有名無名問わず、多くのアプリで同じようなレイアウトの崩れがいくつか見られます。 開発者が意識することなく、うまいことiOS側で管理してほしいものです。
アバター
今回はHTMLメールのテスト工数を短縮するEmail Testing Serviceを紹介したいと思います。 はじめに HTMLメールと聞くと、気が重くなるエンジニアも多いのではないでしょうか。テーブルレイアウトとインラインCSSという普段と違う開発が求められますし、各メーラー、Webメールの表示対応が必須となります。 2016年になっても何かが解決するどころか、確認するデバイスだけが増え続けています。 コーディング → 配信 → サポートクライアントで表示確認 上記をひたすら繰り返し、多くの時間を消費するのを何とかしたい、そんな思いからEmail Testing Serviceを調査し、サービスの運営に取り入れました。 Email Service メールに関するサービスはたくさんあります。 作成を簡単にするためにエディターやテンプレートを提供するサービス 配信を効率よく、簡単にできるようにするサービス 開封率など効果測定、分析を提供するサービス etc 人によって求めている機能は違いますが、現在のプロダクトでは「様々なデバイスでの表示確認に時間がかかっている」という問題が大きいです。そのため今回は HTMLメールのプレビュー機能 について提供しているサービスを紹介します。 Litmus まず最初に Litmus の紹介です。 HTMLのプレビュー機能に特化しているサービスです。 簡単な会員登録を行い、無料で使用することができます。 無料で提供されるクライアントは3種類で「Outlook 2013」「Gmail (Chrome)」「iPhone 6S」でのプレビューが可能です。 HTMLを書きながら各クライアントの見え方が確認できます。 若干反映が遅い気がしますが、それでも十分開発工数の短縮が望めると思います。 個人の感想ですが、使用した中で一番UIが分かりやすく、登録が簡単だったサービスです。 MailChimp 次に MailChimp の紹介です。 HTML作成から配信、効果測定まで提供されており、もちろんプレビュー機能も存在します。プレビュー機能は課金制となっており、tokenという物を購入しないといけません。1つのクライアントを1回テストするのに1token使用し、会員登録の時点では10token付与されます。最小単位の購入で25token/$3と価格自体は安いのですが、どれくらいの頻度でテストをするかによってコストが変わってきます。 とても評判の良いサービスですが、今回はプレビュー機能のみの利便性を求めたため導入は見送りました。「Powered by litmus」とありますが、サポートクライアント数は41ほどでした。 Email on Acid 最後に紹介するのが Email on Acid です。 Email on Acidについては登録しなくても こちら からサンプルが確認できます。 サポートクライアント数が70以上あり、プレビューの切り替えも楽に出来る印象を持ちました。1週間無料のトライアル期間もあり、価格も月額$45からと比較的安い印象を受けました。 まとめ HTMLメールの実装は昔からあまり変わっていませんが、プロバイダー、マーケティング、プレビューといった形でリッチになっている印象を受けました。紹介した3社の優劣は、必要としている機能、規模などによりますので、ぜひご自身にあったサービスを選択してみてください。 今回は海外サービスの紹介となりましたので、日本語でのHTMLメールをテストする際は注意が必要ですが、これらのサービスを使用して少しでもHTMLメールのテストが幸せな物になればと思います。 さいごに VASILYではエンジニアを募集しています。ご興味のある方は是非ご応募よろしくお願いいたします。 https://www.wantedly.com/projects/45354 www.wantedly.com https://www.wantedly.com/projects/42989 www.wantedly.com
アバター
こんにちは、エンジニアの遠藤です。 最近iQONアプリのホーム画面のデザインをリニューアルしました。 タブを使ったデザインにすることで、iQON内にある多くのコンテンツが見やすくなりました。 今回はこのタブ機能の実装についてざっくりと紹介しようと思います。 実装したものはライブラリーとしてGitHubに公開しているので、ぜひ使ってみてください! github.com 機能 今回実装した機能は下記の3つです 1. スワイプでページを無限に表示切り替え 2. タブは無限スクロール 3. タブをタップしたらタップした項目のページを表示 実装について 1. スワイプでページを無限に表示切り替え スワイプしたらページの表示を切り替えたいので UIPageViewController を継承した TabPageViewController というクラスを実装しました。 今回は無限にページの表示切り替えをしたいので UIPageViewControllerDataSource のViewControllerを返すメソッドで調整をします。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] private var beforeIndex : Int = 0 private var currentIndex : Int ? { guard let viewController = viewControllers?.first else { return nil } return pageViewControllers.indexOf(viewController) } override func viewDidLoad () { super .viewDidLoad() // 初期化処理など dataSource = self delegate = self setViewControllers( [pageViewControllers[ 0 ]], direction : .Forward, animated : false , completion : nil ) } } // MARK: - UIPageViewControllerDataSource extension InfinityTabPageViewController : UIPageViewControllerDataSource { private func nextViewController (viewController : UIViewController , isAfter : Bool ) -> UIViewController ? { guard var index = pageViewControllers.indexOf(viewController) else { return nil } if isAfter { index ++ } else { index -- } if index < 0 { index = pageViewControllers.count - 1 } else if index == pageViewControllers.count { index = 0 } if index >= 0 && index < pageViewControllers.count { return pageViewControllers[index] } return nil } func pageViewController (pageViewController : UIPageViewController , viewControllerAfterViewController viewController : UIViewController ) -> UIViewController ? { return nextViewController(viewController, isAfter : true ) } func pageViewController (pageViewController : UIPageViewController , viewControllerBeforeViewController viewController : UIViewController ) -> UIViewController ? { return nextViewController(viewController, isAfter : false ) } } 2. タブは無限スクロール タブは無限でスクロールできるように、表示したい要素数の3倍を用意してスクロール位置がしきい値を超えたら中央に戻します。 今回は UICollectionView をつかって TabView として実装しています。 UICollectionView ではなく UIScrollView でも実装はできるのですが、表示したい要素が増えた時にメモリを圧迫してしまうので採用しませんでした。 // TabView.swift class InfinityTabView : UIView { private var pageTabItemsWidth : CGFloat = 0.0 // MARK: - UICollectionViewDataSource func collectionView (collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int { return pageTabItemsCount * 3 // 表示したい要素数の3倍を返す } func collectionView (collectionView : UICollectionView , cellForItemAtIndexPath indexPath : NSIndexPath ) -> UICollectionViewCell { // Cellを返す処理 } // MARK: - UIScrollViewDelegate func scrollViewDidScroll (scrollView : UIScrollView ) { if pageTabItemsWidth == 0.0 { pageTabItemsWidth = floor(scrollView.contentSize.width / 3.0 ) // 表示したい要素群のwidthを計算 } if (scrollView.contentOffset.x <= 0.0 ) || (scrollView.contentOffset.x > pageTabItemsWidth * 2.0 ) { // スクロールした位置がしきい値を超えたら中央に戻す scrollView.contentOffset.x = pageTabItemsWidth } } } 作成した TabView を TabPageViewController に表示します。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] var pageTabItems : [String] = [] override func viewDidLoad () { super .viewDidLoad() // 初期化処理など let tabView = TabView() tabView.translatesAutoresizingMaskIntoConstraints = false let height = NSLayoutConstraint(item : tabView , attribute : .Height, relatedBy : .Equal, toItem : nil , attribute : .Height, multiplier : 1.0 , constant : TabView.tabViewHeight ) tabView.addConstraint(height) view.addSubview(tabView) let top = NSLayoutConstraint(item : tabView , attribute : .Top, relatedBy : .Equal, toItem : topLayoutGuide , attribute : .Bottom, multiplier : 1.0 , constant : 0.0 ) let left = NSLayoutConstraint(item : tabView , attribute : .Leading, relatedBy : .Equal, toItem : view , attribute : .Leading, multiplier : 1.0 , constant : 0.0 ) let right = NSLayoutConstraint(item : view , attribute : .Trailing, relatedBy : .Equal, toItem : tabView , attribute : .Trailing, multiplier : 1.0 , constant : 0.0 ) view.addConstraints([top, left, right]) tabView.pageTabItems = pageTabItems } } 3. タブをタップしたらタップした項目のページを表示 iQONでは UICollectionViewCell に UIButton をのせた TabCollectionCell を実装しています。 // TabCollectionCell.swift class InfinityTabCollectionCell : UICollectionViewCell { var pageItemPressedBlock : ((index : Int , direction : UIPageViewControllerNavigationDirection ) -> Void )? var pageTabItemButtonPressedBlock : (Void -> Void )? override func awakeFromNib () { super .awakeFromNib() // 初期化処理など } // MARK: - IBAction @IBAction private func pageItemButtonTouchUpInside (button : UIButton ) { pageTabItemButtonPressedBlock?() } } 作成したCellを TabView の UICollectionView に表示していきます。 // TabView.swift class TabView : UIView { var pageItemPressedBlock : ((index : Int , direction : UIPageViewControllerNavigationDirection ) -> Void )? var pageTabItems : [String] = [] } // MARK: - UICollectionViewDataSource extension TabView : UICollectionViewDataSource { func collectionView (collectionView : UICollectionView , cellForItemAtIndexPath indexPath : NSIndexPath ) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(InfinityTabCollectionCell.cellIdentifier(), forIndexPath : indexPath ) as ! InfinityTabCollectionCell configureCell(cell, indexPath : indexPath ) return cell } private func configureCell (cell : InfinityTabCollectionCell , indexPath : NSIndexPath ) { // 無限スクロールのために要素数を3倍用意しているので要素群のindexを計算します let fixedIndex = indexPath.item % pageTabItemsCount cell.title = pageTabItems[fixedIndex] cell.isCurrent = fixedIndex == (currentIndex % pageTabItemsCount) cell.pageTabItemButtonPressedBlock = { [weak self , weak cell] in self ?.pagingViewController(fixedIndex : fixedIndex , nextIndex : indexPath.item ) } } private func pagingViewController (fixedIndex fixedIndex : Int , nextIndex : Int ) { // 遷移する先のページのindexをみてページ送りの方向を決めてからページの表示を切り替えます var direction : UIPageViewControllerNavigationDirection = .Forward if (nextIndex < pageTabItemsCount) || (nextIndex < currentIndex) { direction = .Reverse } pageItemPressedBlock?(index : fixedIndex , direction : direction ) } } TabPageViewController にcellのタップ時の処理を書きます。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] var pageTabItems : [String] = [] override func viewDidLoad () { super .viewDidLoad() // TabViewの初期化処理など      tabView.pageItemPressedBlock = { [weak self ] (index : Int , direction : UIPageViewControllerNavigationDirection ) in self ?.displayControllerWithIndex(index, direction : direction , animated : true ) } } func displayControllerWithIndex (index : Int , direction : UIPageViewControllerNavigationDirection , animated : Bool ) { let nextViewControllers : [UIViewController] = [pageViewControllers[index]] setViewControllers( nextViewControllers, direction : direction , animated : animated , completion : completion ) } } そのほか 機能については上記3つで実装できます。 上記実装のほかにページの表示を切り替え時のタブの位置を移動させるように実装しています。 大まかな実装としては、 UIPageViewController のsubviewsの scrollView を取得してdelegateをselfに設定します。 スワイプした時のscrollViewのdelegate内で処理を書いていきます。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { override func viewDidLoad () { super .viewDidLoad() // 初期化処理など let scrollView = view.subviews.flatMap { $0 as ? UIScrollView }.first scrollView?.delegate = self } } // MARK: - UIScrollViewDelegate extension TabPageViewController : UIScrollViewDelegate { func scrollViewDidScroll (scrollView : UIScrollView ) { // ページを切り替えながらタブの位置を移動させる } } 詳しい実装については説明が長くなってしまうので公開しているコードを見てください。 まとめ 今回はUIPageViewControllerをつかって無限スクロールできるタブUIの実装について紹介しました。 ページの表示を切り替えるだけなどの機能だけならば、比較的シンプルに実装をすることができました。 しかし、ページの表示を切り替えながらタブの位置を移動させようとすると、かなりコード量が増えて苦労しました。 今後同じようなUIを実装しようと考えている方はぜひライブラリーを使ってみてください。 改善要望については、PRやissueをお待ちしています。 GitHub - EndouMari/TabPageViewController: Paging view controller and scroll tab view 最後に VASILYでは、一緒にiQONのアプリを開発してくれる仲間を募集しています。少しでもご興味のある方はぜひご応募よろしくお願いいたします。 https://www.wantedly.com/projects/27397 www.wantedly.com
アバター
Making Sequences work for you こんにちは! VASILYのiOSエンジニア にこらす です。 SwiftのコントリビューターとSwift Evolution SE-0053 の作者です。 他のOSSプロジェクトにも貢献してるので興味があれば Github でフォローしてください。 今回のトピックはSwift2.0以降の SequenceType というプロトコルと、その内部的な動きについて紹介します。 class や struct を SequenceType プロトコルに準拠させると、 for in ループや map , filter などを使えるようになります。 さあ、始めましょう! struct Unique <T: Comparable> { private var backingStore : [T] = [] var count : Int { return backingStore.count } func setHas (element : T ) -> Bool { return backingStore.filter { $0 == element }.count != 0 } mutating func append (element : T ) { guard ! setHas(element) else { return } backingStore.append(element) } } この Unique という構造体はとてもシンプルです。 Comparable に準拠した型の要素を append すると内部的な配列に要素を追加します。配列中に同じ要素が存在する場合は追加しません。 それでは Unique をテストしてみましょう! var mySet = Unique < String > () // Our set can mySet.setHas( "A Careless Cat" ) //false mySet.append( "A Careless Cat" ) mySet.setHas( "A Careless Cat" ) //true mySet.append( "A Careless Cat" )  //すでにある文字列 mySet.count //まだ1です! //もうちょっと項目追加しましょう! mySet.append( "A Dangerous Dog" ) mySet.append( "A Diamond Dog" ) mySet.append( "Petty Parrot" ) mySet.append( "North American Reckless Raccoon" ) mySet.append( "A Monadic Mole" ) 動物は十分入ったので、名前を print してみよう! for animal in mySet { println(animal) } あれ?うまくいきませんね。。。: なぜうまくいかなかったかというと、 for in を使うために必要な実装が足りないからです。 Stringの配列で for in を書くのは、下記の書き方と同じです。 let vowels = [ "A" , "E" , "I" , "O" , "U" ] var gen = vowels.generate() while let letter = gen.next() { print(letter) } generate() は SequenceType プロトコルのメソッドです。 Unique は SequenceType プロトコルに準拠してないので for in が動かないのです。 それでは、 SequenceType プロトコルに必要な条件を見て実装してみましょう! public protocol SequenceType { associatedtype Generator : GeneratorType public func generate () -> Self.Generator } generate() の戻り値の型は型推論が効くので、 typealias Generator = ... を書く必要はありません。 Unique を SequenceType プロトコルに準拠させて、 generate() メソッドを実装するべきですが、まだ GeneratorType のことをよく知りません。 それでは GeneratorType をチェックしましょう! Generator とは? public protocol GeneratorType { associatedtype Element public mutating func next () -> Self.Element ? } SequenceType と同じように一つのメソッドしかありません。 next() の戻り値の型は、 Element 型となっていますが型推論が効きます。 [String] であれば、実際には String になります。 Generator は next() メソッドで、保持しているデータを順番に返します。最後のデータを返したあと再度 next() を呼ぶと nil を返します。 GeneratorType を使い終わると再利用できません。同じデータセットをもう一度 GeneratorType で読みたければ、新しく GeneratorType を生成する必要があります。 そして、 generate() を実装する時に戻り値の GeneratorType と他の GeneratorType 変数の状態を共有しないようにしなければいけません。 基本的な上限がある GeneratorType : struct CountToGenerator : GeneratorType { private var limit : Int private var currentCount = 0 init (limit : Int ) { self .limit = limit } mutating func next () -> Int ? { guard currentCount < limit else { return nil } defer { currentCount += 1 } return currentCount } } var goTillTen = CountToGenerator(limit : 10 ) while let num = goTillTen.next() { print(num) } SequenceType と GeneratorType を学んだので、 Unique 専用の GeneratorType を作りましょう! class UniqueGenerator <T>: GeneratorType { private var _generationMethod : () -> T ? init (_generationMethod : () -> T ?) { self ._generationMethod = _generationMethod } func next () -> T ? { return _generationMethod() } } extension Unique : SequenceType { func generate () -> UniqueGenerator <T> { var iteration = 0 return UniqueGenerator { if iteration < self .backingStore.count { let result = self .backingStore[iteration] iteration += 1 return result } return nil } } } ここまで学んだことで、 Unique の GeneratorType の実装ができましたが、もう少し短く書けます。 AnyGenerator というGenericタイプを使うと UniqueGenerator を作る必要がありません。 extension Unique : SequenceType { func generate () -> AnyGenerator <T> { var iteration = 0 return AnyGenerator { if iteration < self .backingStore.count { let result = self .backingStore[iteration] iteration += 1 return result } return nil } } } もう一度 for in ループを書きましょう! for item in mySet { print(item) } やっと動きました! map と filter も動きます!やった! let cnt = mySet.map { Int( $0 .characters.count) } //[14, 15, 13, 12, 31, 14] mySet.filter { $0 .characters.first != "A" } //["Petty Parrot", "North American Reckless Raccoon"] Controlling Sequences with Sequences SequenceType の実装次第で、とても大きいリストや無限リストを作ることができます。 そういったリストから先頭から n 個取り出そうとすると、そのまま使うと無限ループになってしまいます。 例えば下記の ThePattern は無限リストです。 for in で使うと、 Generator が nil を返さないため、永遠に 0 か 1 を返します。 class ThePattern : SequenceType { func generate () -> AnyGenerator <Int> { var isOne = true return AnyGenerator { isOne = ! isOne return isOne ? 1 : 0 } } } // 無限ループ for i in ThePattern() { print(i) } この無限リストから class First <S: SequenceType>: SequenceType { private let limit : Int private var counter : Int = 0 private var generator : S.Generator init (_ limit : Int , sequence : S ) { self .limit = limit self .generator = sequence.generate() } func generate () -> AnyGenerator <S.Generator.Element> { return AnyGenerator { defer { self .counter += 1 } guard self .counter < self .limit else { return nil } return self .generator.next() } } } for item in First( 5 , sequence : ThePattern ()) { print(item) // 0 1 0 1 0 } いいですね! First(n, sequence: s) を呼び出すと、 s の先頭 n 個の要素を取り出せます。 s が無限リストだとしても最初の n 個しかチェックしないので効率的です。 まとめ 配列のように扱うクラスやコンテナクラスであれば SequenceType プロトコルを採用しましょう。 コードが読みやすくなりますし、自然に for in や map , filter を使えるようになります。 ぜひ気軽に SequenceType のプロトコルを使ってみてください。 最後に VASILYではSwift好きなiOSエンジニアを募集しています!興味があればぜひ応募してみてください。 https://www.wantedly.com/projects/27397 www.wantedly.com
アバター
2016年3月22日、第二回目となる Fashion Tech meetup を開催しました。前回は MERY を運営する株式会社peroli様との開催でしたが、今回は FRIL を運営する株式会社Fablic様が加わり、VASILYを含め3社での開催となりました。 イベント公開開始時、参加枠70席のところ120名を超える申し込みがあり、増枠を設けるほどの大盛況となりました。 最終的に180人を超える申し込みを頂き、FashionxTechnologyに少しずつ関心が持たれているのではないかと感じています。 弊社からも3名が登壇し、Fashion x Technologyを題材とした発表を行ってきました。 今回はその資料を公開します。残念ながら参加出来なかった方、Fashion Tech meetupを初めて知った方、是非ご一読ください。 メインセッション 「ディープラーニングを使って商品カテゴリの分類をしてみました」 弊社の新卒エンジニアの塩崎と、内定者インターンの後藤が発表しました。 塩崎は原子核物理学、後藤は観測的宇宙論・銀河形成論というファッションとはかけ離れた研究をしていた2人が、ディープラーニングでファッションのカテゴリを分類していくという発表になっています。 LT ファッションアイテムの類似画像検索を実装してみました こちらは2016年3月より弊社に入社したデータサイエンティストの中村が類似画像検索についてLTを行いました。まだ入社して3週間ですが、さっそく自身の知識と探究心で、技術的なアプローチをしています。 まとめ VASILYの資料を公開しましたが、いかがでしたでしょうか。 ファッションやWeb業界と大きく異なる経歴を持つ若いメンバーが、FashionxTechnologyに対して様々なアプローチをして活躍していることは伝わったのではないかと思います。 今回弊社の資料を公開しましたが、peroli様、Fablic様の発表も面白く、各社の個性がでたFashion Tech meetupでした。来場された方もファッションに関わる方、技術に興味を持つ方の集まりなので懇親会も活発なものでした。 Fashion Tech meetupを通して、少しずつ業界を盛り上げていけるのではないかと思います。 本記事で興味を持ち、Fashion Tech meetup #3が開催される際に足を運んでくださる方が増えれば幸いです。 おわりに 弊社の資料を通して興味を持たれた方、是非一度オフィスまでお越しください。 一緒にFashionxTechnology盛り上げていきましょう! https://www.wantedly.com/projects/45354 www.wantedly.com https://www.wantedly.com/projects/42989 www.wantedly.com
アバター