TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは、バックエンドエンジニアのじょーです。 以前、月額課金型のサーバーサイドでのレシート検証の記事を書きました。( iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ ) 今回は、消耗型課金のサーバーサイド実装について書きます! 注意 この情報は2017年8月25日現在のものです。 目次 消耗型課金全体の処理フロー レシート検証について 課金アイテムの扱い方について 消耗型課金全体の処理フロー 消耗型課金とは、AppStoreで登録できる使い切りのアイテムへの課金のことをいいます。 たとえば、ゲームアプリでライフを購入するときなどは使い切りのアイテムなので消耗型課金になります。一方、1か月など決まった期間サービスが受けられる課金のことを月額課金や、自動更新購読といったりします。 (実際のアイテム登録画面) アプリで消耗型課金商品を購入してからの処理の順番は、下記の図のようになります。 アプリからAppStoreへ購入リクエスト AppStoreからレシートを受取る サーバーサイドへBase64エンコード済みのレシートを投げる サーバーサイドはBase64エンコード済みのレシートを使ってAppStoreにレシート問い合わせ AppStoreからJSON形式のレシートを受取る レシート情報を元に、購入が不正に行われたものでないかを検証 正常な購入であれば購入結果をDBに反映 アプリに検証結果を返却 処理完了のリクエストをAppStoreへ送信 また、本記事ではサーバーサイドで行う処理に着目しているため、工程の①〜③に関しての詳細説明は割愛し、サーバーサイドで行う④〜⑥のレシート検証処理の工程を詳しく説明していきます。 レシート検証について レシート検証とは? レシート検証とは、アプリから購入リクエストが届いた際、アプリを介さずにAppStoreに直接購入情報を問い合わせることで、不正な購入や意図しない購入でないかを検証することです。この処理によって、アプリから偽装されたレシートが送られてきた場合や二重の購入、購入処理の漏れなどを防ぐことができます。 アプリで商品を購入した場合、課金の証明としてAppStoreがレシートを発行します。 レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。 その際に、AppStoreのサーバーにHTTPのPOSTリクエストでJSON形式のレシートを問い合わせ、現在の課金状況を知ることができます。その結果を元に、レシートが不正なレシートでないかをサーバーサイドでチェックします。 レシート問い合わせの方法 処理フローでの④,⑤について説明します。 レシート情報の問い合わせは、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 下記の1つだけでOKです。 key 値 サンプル receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI... receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。 処理フローの③で示した通り、クライアントからBase64エンコード済みのデータを受け取ります。購入時にクライアントからレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。 AppStoreから返ってくるレシート情報の項目 下記が、レシートを問い合わせた際に返ってくるレシートサンプルです。処理フローでいうと⑤でAppStoreが返す情報です。 RubyのHash型で記してありますが、実際にはJSON形式で返ってきます。 { " status " => 0 , " environment " => " Sandbox " , " receipt " => { " receipt_type " => " ProductionSandbox " , " adam_id " => 0 , " app_item_id " => 0 , " bundle_id " => " jp.hoge.hoge " , " application_version " => " 1 " , " download_id " => 0 , " version_external_identifier " => 0 , " receipt_creation_date " => " 2017-07-18 04:03:48 Etc/GMT " , " receipt_creation_date_ms " => " 1500350628000 " , " receipt_creation_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " request_date " => " 2017-07-18 05:32:59 Etc/GMT " , " request_date_ms " => " 1500355979599 " , " request_date_pst " => " 2017-07-17 22:32:59 America/Los_Angeles " , " original_purchase_date " => " 2013-08-01 07:00:00 Etc/GMT " , " original_purchase_date_ms " => " 1375340400000 " , " original_purchase_date_pst " => " 2013-08-01 00:00:00 America/Los_Angeles " , " original_application_version " => " 1.0 " , " in_app " => [ { " quantity " => " 1 " , " product_id " => " productのid " , " transaction_id " => " 1284721948247 " , " original_transaction_id " => " 1000000316178057 " , " purchase_date " => " 2017-07-18 03:20:05 Etc/GMT " , " purchase_date_ms " => " 1500348005000 " , " purchase_date_pst " => " 2017-07-17 20:20:05 America/Los_Angeles " , " original_purchase_date " => " 2017-07-18 03:20:05 Etc/GMT " , " original_purchase_date_ms " => " 1500348005000 " , " original_purchase_date_pst " => " 2017-07-17 20:20:05 America/Los_Angeles " , " is_trial_period " => " false " }, { " quantity " => " 1 " , " product_id " => " productのid " , " transaction_id " => " 1284721948248 " , " original_transaction_id " => " 1000000316185518 " , " purchase_date " => " 2017-07-18 04:03:48 Etc/GMT " , " purchase_date_ms " => " 1500350628000 " , " purchase_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " original_purchase_date " => " 2017-07-18 04:03:48 Etc/GMT " , " original_purchase_date_ms " => " 1500350628000 " , " original_purchase_date_pst " => " 2017-07-17 21:03:48 America/Los_Angeles " , " is_trial_period " => " false " } ] } 今回は、レシート検証(処理フローで言う④,⑤の部分)に venice というgemを使いました。こちらのgemを使うと、開発環境ではsandboxを、本番環境ではproductionのエンドポイントを叩いてくれたり、JSONをパースしてRubyのHash型でレシートの値を返してくれます。 検証項目 ⑤で返ってきたJSON形式のレシートを使って、サーバーサイドで検証すべき内容を検証します。処理フローで言うと、⑥の部分です。 レシートから返ってきた項目のうち検証すべき項目は以下です。 項目 項目の内容 検証内容 status 0であれば正常なレシート、その他は不正なレシート( エラーコード表 参照) AppStoreから正常なレシートが返ってきているか in_app 購入情報の配列 課金処理すべき購入情報のチェック in_app: transaction_id in_appの1購入ごとに存在する固有のid すでに処理されたレシートでないか bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か、存在している商品への課金か 消耗型課金の場合、上記の表以外の項目はデバッグに用いる値と考えて良いと思います。他に検証すべき項目があればコメントをください。よろしくお願いします。 これらの項目の中で注意すべきは、 in_app キーの中身です。 in_app の中身は、購入1回分の購入情報の配列です。注意しなければならないポイントは、 in_app の配列の中身が複数返ってくることがあるというところです。 AppStoreでは、①〜⑨までの処理にトランザクションをはっており、どこかで通信エラーやサーバーエラー等のエラーが起きて⑨の完了処理を実現できなかった場合、トランザクションが完了せずに未処理の購入となります。 未処理の購入は下記の2パターンの方法で再度処理を行う必要があります。 ⑨が実現できていないという情報がStoreKitからアプリに通知されたタイミングで再度③から課金処理を行う 別のアイテムの購入処理をした際に、AppStoreから未処理の購入情報も一緒に返ってくるのでそのタイミングで課金処理を行う(トランザクションが完了していない購入がある場合同じアイテムは購入できない) パターン2の場合、別の購入処理で問い合わせたレシートの in_app の中に未処理の購入が含まれて返ってきます。 この場合、 in_app の中に返ってくる購入情報は新しく購入されたか、一度処理を失敗して残っている情報なので in_app の中身すべてを処理する必要があります。 また、下記の図のように、サーバーサイドのアイテム付与の処理はすべて終了しているにも関わらず、⑧で通信が失敗してしまい、⑨の処理が完了していないというパターンが存在します。その際、サーバーサイド側に過去に処理した transaction_id を保持しておき、過去に処理した transaction_id であればサーバーサイドでは二度同じ処理をせずに⑨の購入完了処理のみをアプリ側に行ってもらうようにしておく必要があります。 課金アイテムの扱い方について 消耗型課金は、課金アイテムが一つでない限り、課金アイテムのリストをアプリに表示する必要があります。たとえば、ルビー1200個購入で10000円、12個で120円など、アイテムが複数ある場合です。 その際に、AppStoreにないアイテムはそもそも購入できないため、アイテムがAppStoreに存在しているかをチェックしてからアイテムリストをアプリに表示します。 また、逐一AppStoreにアイテムリストを問い合わせる理由として、価格の変化があります。AppStoreで登録した商品は、為替レートの変動により価格が変わることがあるので、毎回現状の価格をAppStoreに問い合わせなければアプリで表示している価格と実際にAppStoreで決済される価格が食い違ってしまいます。 AppStoreへの課金アイテム問い合わせフロー このような手順で課金アイテムリストを表示します。 AppStoreにアイテムを登録する際、アイテムを識別するユニークなkey名を product_id と呼びます。 また、課金アイテムリストは、サーバーサイドでも管理します。 AppStoreへアクセスすればサーバーサイドでアイテムリストを管理する必要がないのでは?と思うかもしれませんが、サーバーサイドでもアイテムリストを持ったほうがいい理由は3つあります。 AppStoreでは最小限の商品情報しか持てない たとえば、 ルビー1200個購入で10000円 という商品の場合、 ルビー1200個 という情報はAppStoreでは登録できず、参照名、product_id(製品ID)と価格くらいしか保持していません。(下図参照) よって、サーバーサイドでproduct_idと商品の内容を紐付けたテーブルをDBに持つ必要があります。 課金履歴などのデータを保持しやすくなる 期間限定のアイテムなどを配信したい際にサーバーサイドでコントロールできる (課金アイテムの登録画面) 参照名、製品ID(product_id)、価格が登録できます。 まとめ 本記事では、消耗型課金におけるサーバーサイドに必要な実装について説明しました。 参考資料 In-App Purchaseプログラミングガイド Validating Receipts With the App Store ※ バージョンによって仕様が変わるので、もともとの仕様書をよく読むことをおすすめします。 最後に VASILYでは、積極的に挑戦していけるエンジニアを募集しています。興味のある方は以下のリンクからぜひご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
アバター
こんにちは、データチームの後藤です。 VASILYデータチームは2017年8月7日〜10日にかけて、広島で行われた第20回画像の認識・理解シンポジウム(以下、MIRU2017)に参加しました。本記事では、発表の様子や参加した感想をお伝えしたいと思います。 MIRU2017 MIRUはMeeting on Image Recognition and Understandingの略で、国内最大規模の画像の認識と理解技術に関する会議です。事前に選定された口頭発表と国際会議の採択論文から選ばれた招待講演を中心に、ポスター発表、デモ発表、特別講演で構成されます。 今年の投稿件数は、以下のような内訳でした。 口頭発表:23件 一般論文:206件 デモ論文:13件 招待講演:12件 企業展示:15件 VASILYのデータチームは「トリプレット損失関数の重み付けによる学習の効率化」というタイトルで一般論文の枠で投稿し、4日目の8月10日にポスター発表を行いました。 発表 ポスターセッションでは始終、多くの人に話を聞いていただき、VASILYの取り組みを伝えることができました。今回の我々の研究は数式と文字による説明が多く、ひと目でわかりやすい研究ではなかったかもしれませんが、皆様の積極的に理解しようとする姿勢がとてもうれしくやりがいがありました。研究者の方々からその場で改善に関する貴重なアドバイスをいただいたり、ファッションと機械学習について改めて意見交換をする話になったりと、実りのある時間になったと思います。技術的な内容については別の記事で扱いたいと思います。 MIRU2017 発表ポスター 感想 国内外の素晴らしい研究成果を学ぶことで、研究のアイデアを多く得られたことがなによりの収穫でした。弊社の機械学習を使ったプロダクトでは物体検出や生成モデル、属性予測モデルなどでCNNを活用していますが、その高度化に使えそうな研究が多くありました。招待講演は、すでに有名な国際会議で発表されている研究を紹介する企画でしたが、勉強不足で把握していなかった研究やアイディアが多くあったため、とても有難かったです。弊社では早速、今回の学会参加で得られた知見を取り入れる実験を開始しています。 また、ファッションの画像を使った研究に取り組んでいる研究者や学生と議論や意見交換をできたことも良かったです。ファッションの画像の研究という特定の分野に携わっている人たちが集まる機会はなかなか無いため、論文として発表されない限り、どこでどんなことに取り組まれているのかを知ることはできません。MIRUに参加することで、様々な研究機関の取り組みと取り組んでいる人たちを把握できたことは貴重な機会でした。 気になった研究 個人的に興味をもった研究を簡単にご紹介します。 [PS1-28] キャッシュ型k-d木:クエリの偏りを用いた近似最近傍探索法 香川 椋平, 和田 俊和(和歌山大) 近似最近傍探索において、登録データの統計的分布を利用する手法は多くある一方で、クエリの統計的偏りは検討されてきませんでした。この研究では、k-d木による探索のうち、木探索とpriority searchの結果が異なるクエリをキャッシュすることで時間の掛かるpriority searchを省略するという方法を提案し、k-d木による探索の高精度化・高速化に成功しました。弊社でも大量の画像を検索するタスクが多いため、クエリの偏りを利用してキャッシュしておくというアイデアは利用できそうです。 [DS-3] キーポイントの幾何学的一貫性を考慮した実時間画像検索システム 大倉 有人, 和田 俊和(和歌山大) 画像検索において、クエリにカメラで撮影した画像を用いる場合、検索したい物体以外のものが含まれてしまうことが多いです。この研究では、データベース上の画像とクエリ画像の、SURFで検出されたキーポイントの各組から相似変換を推定し、推定された各相似変換を集計します。最も投票の多かった相似変換に従うキーポイントの組を採用することで、クエリ画像上の検索対象と無関係な背景の効果を実時間のうちに除去することが可能になりました。 [OS4-1] Dynamic Fashion Cultures Kaori Abe, Teppei Suzuki, Shunya Ueta(AIST), Akio Nakamura(Tokyo Denki Univ.), Yutaka Satoh, Hirokatsu Kataoka(AIST) この研究では、世界中のファッショントレンドを時系列で分析・可視化することを目的とし、位置と時間の情報を持った人物画像の巨大データベースを作成しました。Yahoo! Creative Commons 100M Databaseから人物を切り出し、タイムスタンプとジオタグを付与しています。画像特徴量をクラスタリングして、各クラスタの数の時系列変化を見ることで、特徴的なファッショントレンドを抽出しています。 この研究では1年ごとのトレンドを見ていますが、ファッション業界からは1週間スケールでのトレンドを把握したいという要望があるようです。これは非常に難しい課題ですが、応用第一のファッションテックにおいて、とても重要な課題になるでしょう。 [PS2-27] ディープラーニングによるファッションコーディネートの分類 高木 萌子, シモセラ エドガー, 飯塚 里志, 石川 博(早大) ファッションアイテムの推薦はユーザーのスタイルを把握していなければ難しいタスクです。この研究では、13000枚以上のトータルコーディネートの画像を収集し14種類のファッションカテゴリに分類したデータセットを作成し、CNNを用いたファッションカテゴリ判別器を学習しました。ファッションの専門家と素人の判別精度の中間程度の精度を達成しています。ポスター発表ではCNNが判別の際に注目している位置を可視化しており、その判断基準には納得感のあるものが多かったです。弊社の推薦システムにおいても、ユーザーの好むファッションスタイルを理解することが重要なので、この研究を発展させていきたいです。 [PS1-59] 衣服情報の活用による姿勢推定の精度向上に関する研究 金子 直史, 鷲見 和彦(青学大) この研究では、人の体型を画像から測る目的から、精度の高い姿勢推定のネットワークを作成しています。通常の姿勢推定用のネットワークの出力に、衣服のセグメンテーションを行った結果を加えることでより精度の高い姿勢推定を行うネットワークを実現しました。この研究では2つのネットワークを単純に合わせたものが提案されていましたが、姿勢推定の誤差を修正するネットワークも有効なのではないかという議論をしました。 [OS5-6] 2D-QRNNを導入したDCNNによるセマンティックセグメンテーションの高精度化と高速化 古川 弘憲, 山下 隆義, 山内 悠嗣, 藤吉 弘亘(中部大), 石井 育規, 羽川 令子(パナソニック) 局所的な情報を活用するCNNに、文脈情報を考慮できるRNNを組み合わせることにより、セマンティックセグメンテーションの精度を上げられることが知られています。しかし、通常のRNNでは並列演算が出来ず、推論に掛かる時間のボトルネックとなってしまいます。この研究では、二次元に拡張したQuasi-Recurrent Neural Networksを用いることで、DAG-RNNと同程度の精度を保ちながら推論にかかる時間を29%まで削減できました。 [IT-8]【招待講演】Generative Attribute Controller With Conditional Filtered Generative Adversarial Networks (CVPR2017) Takuhiro Kaneko, Kaoru Hiramatsu, Kunio Kashino 論文: http://openaccess.thecvf.com/content_cvpr_2017/papers/Kaneko_Generative_Attribute_Controller_CVPR_2017_paper.pdf この研究では、Conditional GANを拡張し、ノイズ変数を属性情報でフィルタリングするアーキテクチャを加えたCFGANを提案しています。画像の属性情報を操作して、画像を変換するタスクにおいて、提案手法は属性の表現力と操作性を向上させることに成功しています。 フィルタリングの方法を変えることにより、ラジオボタンやスライドバーによる変換など、従来よりも操作性の高い方法を実現できています。ファッションの画像検索においても、このような属性変換による画像検索は実用性が高いと考えます。 [OS5-5] Segmentation based on Transform Invariant Auto-encoder Tadashi Matsuo, Nobutaka Shimada(Ritsumeikan Univ.) この研究では、上図のように平行移動やバックグラウンドに不変なオートエンコーダーを提案しています。変換無し画像の復元画像と、平行移動などの変換を施した画像の復元画像が近くなるように設計したTransform variance termと、変換を施した画像と復元画像が近くなるよう設計した Restoration error termをコスト関数に用いて学習します。 設計の意図の通り、入力画像の位置が異なっても(上のFig.2)、出力はほとんど同じものとなっています(上のFig.4)。 [PS2-20] 適応的ランク誤差に基づくTriplet 局所特徴表現学習 田良島 周平, 黒住 隆行, 杵渕 哲也(NTT) 通常のトリプレット損失関数は、アンカー(a)と負例(n)の距離がアンカーと正例(p)の距離よりも固定のマージン以上に小さい場合に誤差をとる損失関数です。しかし、上図の左のように各距離がマージンよりも十分に大きい場合、相対的にマージンの効果が小さくなり、誤差が発生しない状況が増えてしまいます。この研究では、固定していたマージンをアンカーと正例の距離に対して適応的に変化させることで、各距離がマージンよりも十分に大きい場合の弁別性の向上を狙いました。提案手法は、パッチペアの分類推定と射影変換推定のタスクにおいて、Hard miningを行う手法以外の多くの場合で、既存手法を上回る性能を発揮しています。 おまけ 本記事の趣旨とはずれますが、学会の合間に広島を満喫しましたので写真で紹介します。 厳島神社 干潮時の厳島神社の鳥居。カニを捕まえるのに夢中になってしまいました。 鹿(宮島) 宮島は鹿が出迎えてくれます。我々と一緒に歩いてくれる優しい鹿もいました。 お好み焼き(ロペズ) 横川のロペズでお好み焼きを食べました。とても有名なお店だそうで、一時間近く並びました。ハラペーニョをトッピングした風変わりなお好み焼きを提供してくれます。 あなご竹輪(宮島) あなご感、特になし…!! 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。本記事のように、業務の一環として学会に参加することを認められる環境です。 興味のある方はこちらからご応募ください。
アバター
こんにちはフロントエンジニアの茨木です。一ヶ月ほど前からSwiftでiOSアプリ開発をやっています。iOS開発経験は浅いですが、Lottieというライブラリを使用し、いきなりアニメーションごりごりの画面を担当してみました。 Lottie はAirbnb社が開発したライブラリで、僅かなコードでアニメーションを実装できるスグレモノです。 本記事では、SwiftにおけるLottieの使い方を説明します。 Lottieの特徴 Lottieの最も大きな特徴は、Adobe After Effectsで出力したアニメーションデータをそのまま読み込むだけでアニメーションを実装できることです。その為、沢山のコードや画像が不要なのはもちろんのこと、デザイナーが作ったアニメーションを忠実に再現することが可能です。更に、LottieはAndroidやReact Nativeもサポートしているので、クロスプラットフォームでアニメーションを再利用することもできます。 Lottieの導入方法 アニメーションデータの出力 Lottieでアニメーションデータを読み込むには、Adobe After Effectsの拡張機能 Bodymovin を使ってJSON形式で出力する必要があります。出力の詳細な手順はここでは割愛します。また、LottieはAfter Effectsの全機能をサポートしているわけではないので注意が必要です。詳しくは こちら をご覧ください。 アニメーションの実装 とりあえず実装してみる 出力したJSONファイルをXcodeプロジェクト内の任意の場所に置き、アニメーションを実装したいViewControllerにコードを4行追加するだけです。 import UIKit // ライブラリをインポート import Lottie final class LottieExampleViewController : UIViewController { override func viewDidLoad () { super .viewDidLoad() // アニメーションのviewを生成 let animationView = LOTAnimationView(name : "heartAnimation.json" ) // ViewControllerに配置 self .view.addSubview(animationView) // アニメーションを開始 animationView.play() } } LOTAnimationView(name: String) の引数nameにはJSONのファイル名を指定します。拡張子はなくても問題ありませんが、ファイル名にパスを含めてしまうと読み込みに失敗します。また、外部データを取得する場合は LOTAnimationView(name: String) の代わりに LOTAnimationView(contentsOf: URL) を使います。 LOTAnimationViewについて LOTAnimationViewはUIViewを継承しており、デフォルトの幅・高さはアニメーションの幅・高さと同じになります。LOTAnimationViewは、通常のviewのようにframeやAutoLayoutでレイアウトしたり、contentModeでビュー内のコンテンツ配置を調整したりできます。 更に、LOTAnimationViewにはアニメーションに関する様々なオプションがあり、ループや速度の設定が可能です。設定できるオプションは以下のようなものがあります。 LOTAnimationViewのプロパティ 詳細 animationDuration: CGFloat { get } アニメーションの開始から終了までの時間を取得できます。 animationProgress: CGFloat { get set } 開始を0.0、終了を1.0とした場合のアニメーションの経過時点を取得/指定できます。値を指定した場合、指定時点で静止したアニメーションが表示されます(isAnimationPlayingがtrueの間は指定しても静止しません)。 animationSpeed: CGFloat { get set } 元の速度を基準としたアニメーションの速度を取得/指定できます。 loopAnimation: Bool { get set } アニメーションのループの有無を取得/指定できます。 isAnimationPlaying: Bool { get } アニメーションが動作中かどうかを取得できます。 実装例 「いいね!」をタップした時などに出てきそうなハートのアニメーションを実装してみた例です。 このようなアニメーションも、Lottieを使えばレイアウトの調整含め5行ぐらいのコードで実装できます。 まとめ Lottieによりアニメーションの実装コストが大幅に削減されました。更に、デザインに忠実なアニメーションの実装が可能になりました。とても便利なライブラリなので、読者の皆様も一度試してみることをオススメします。Lottie用のアニメーションは LottieFiles でダウンロードすることもできるので、アニメーションを作るのはちょっと…という方も是非試してみて下さい。 最後に VASILYではやったことないことにも果敢にチャレンジできるようなエンジニアを募集しています。 興味がある方はこちらからご応募ください。
アバター
こんにちは、フロントエンドエンジニアの権守です。 既にお気づきの方も多いと思われますが、こちらのテックブログは今月から装いを新たにしています。これは 先日行った弊社コーポレートサイトのリニューアル に合わせたものです。 この記事では、今回行ったコーポレートサイトリニューアルについて実装面から紹介します。 特徴 今回のリニューアルの特徴は以下の3つです。 Vue.jsによるSPA (Single Page Application)としての実装 レスポンシブ対応 アニメーションによるリッチな表現 それぞれについて詳しく紹介していきます。 Vue.js Vue.js はユーザーインタフェースを構築するためのJSフレームワークの1つです。Reactなどのフレームワークと同様にSPAの実装に使えますが、段階的に導入していくことも可能な点が特徴です。 その段階的に導入可能という点が、既存のサービスへの導入を容易にしていることもあり、弊社ではVue.jsを一部導入しています。現状、段階的に使っている上ではVue.jsは非常に便利という印象でしたが、今後、本格的に導入していく上でSPAフレームワークとしても満足のいくものであるかはわかりませんでした。 そこで、今回はVue.jsのSPAフレームワークとしての使い勝手を検証する意味も込めてコーポレートサイトの構築に全面的に導入しました。 Vue.jsの優れた点 結論から言うと、Vue.jsはSPAフレームワークとしても非常に便利であり、今後SPAを開発する際に弊社で採用する可能性は非常に高いと思います。 その判断に至った大きな要因は 単一ファイルコンポーネント による関心の分離のスマートさです。Vue.jsの単一ファイルコンポーネントでは、 こちら で述べられている「関心の分離はファイルタイプの分離と等しくない」という理論の元、作られています。具体的には1つのファイルにテンプレート(HTML)・ロジック(JavaScript)・スタイル(CSS)の記述を可能としており、一貫性と保守性を高めています。 以下にコーポレートサイト内で用いている単一ファイルコンポーネントの例を示します。 main_heading.vue < template lang = "pug" > h1 {{title}} </ template > < script > export default { name: 'heading' , props: [ 'title' ] , } </ script > < style lang = "sass" scoped> @media ( max-width : 1039px ) h 1 display: none @media ( min-width : 1040px ) h 1 color : # 333 text-align: center font-family: 'DIN' font-weight: bold font-size: 30px letter-spacing: 0.75px line- height : 200px </style> 上の例からテンプレート・ロジック・スタイルが1つのファイルに書かれていることがよくわかると思います。また、style部分にscopedと書かれていますが、これは単一ファイルコンポーネントが有するScoped CSSという機能を利用することを示しています。この機能を利用することで、そのコンポーネント内に記述されたスタイルが他のコンポーネントを汚染することを心配する必要がありません。それによってネームスペースを確保するためだけの余計なクラスを付与する必要もなくなります。これらの徹底した関心の分離はアプリケーションが大規模になるほど、より効果的になると考えています。 レスポンシブ対応 PC、タブレット、スマホといった具合にサイトを訪れる方のデバイスは様々です。それらのどれでアクセスしたとしても、弊社の魅力を最大限伝えられるように、レスポンシブ対応を全面的に行いました。 CSSのメディアクエリを用い、画面サイズによってPC用とモバイル用でスタイルを切り替えています。また、一部コンポーネントではモバイル版の中でもサイズによってレイアウトを2カラムから3カラムに切り替えるといったことも行っています。 CSSだけでなく、 以前書いた記事 で紹介した画像のレスポンシブ対応ももちろん取り入れています。 実際にどのようにCSSによるレスポンシブ対応をしているかの雰囲気がわかるように2カラムから3カラムに切り替わるコンポーネントのスタイル部分を示します。 article_card.vue < style lang= "sass" scoped > article display : block overflow : hidden text-align : left . container position : relative width : 100% overflow : hidden img position : absolute top : 50% left : 50% -webkit- transform : translate( -50% , -50% ) transform : translate( -50% , -50% ) height : 100% margin : auto . blog display : inline-block color : #333 font-family : 'DIN' font-weight : bold & : hover text-decoration : underline time display : inline-block float : right color : #999 font-size : 10px . title display : block display : -webkit-box -webkit- box-orient : vertical -webkit-line-clamp: 3 overflow : hidden color : #333 text-align : left @media ( max-width : 330px ) article .title font-size: 10px line- height : 15px @media ( min-width : 331px ) and ( max-width : 1039px ) .title font-size: 11px line- height : 17px @media ( max-width : 557px ) article &:before padding-top: 153.62% width : calc( 50% - 5px ) @media ( min-width : 558px ) and ( max-width : 1039px ) article &:before padding-top: calc( 100% + 90px ) width : calc( 33.3% - 6.66px ) @media ( max-width : 1039px ) .meta padding: 0 3px 0 5px .blog font-size: 12px margin-top: 13px .title margin-top: 7px time margin: 14px 2px 0 0 article >a width : 100% height : 100% display: block position: absolute top: 0 left: 0 &:before display: block content: '' .container &:before display: block content: '' padding-top: 100% @media ( min-width : 1040px ) article width : 260px height : 310px transition: margin-top 0 . 25 s ease-out &:hover margin-top: -20px .container height : 200px .blog font-size: 13px margin-top: 15px time margin-top: 15px .title font-size: 13px line- height : 20px margin-top: 9px </style> また、このレイアウトが切り替わった際の要素の再配置部分は Masonry を用いて実装しました。CSSだけで実装しなかったのは、高さの異なる要素を縦方向に詰めて配置することができなかったからです。 アニメーションによるリッチな表現 今回のサイトデザインは、読みやすさを重視したシンプルなデザインな一方で、冷たい印象になりすぎないようにリッチなアニメーションを散りばめています。アニメーションの実装にはCSS3のアニメーションを中心に使いつつ、より複雑なアニメーションが求められる箇所では D3.js を用いました。 2つピックアップして実装について紹介します。 lazy-box スクロールに応じて、コンテンツが出現するアニメーションをlazy-boxというコンポーネントとして実装しました。 lazy_box.vue < template lang = "pug" > .lazy-box(ref="lazyBox" :class="{active: active}") slot(:active="active") </ template > < script > export default { mounted () { window .addEventListener ( 'scroll' , this .handleScroll ) ; this .handleScroll () ; } , destroyed () { window .removeEventListener ( 'scroll' , this .handleScroll ) ; } , data () { return { active: false , handleScroll: ( e ) => { if ( window .innerHeight - this .$refs.lazyBox.getBoundingClientRect () . top > 0) { this .active = true ; } } } } } </ script > < style lang = "sass" scoped> .lazy-box opacity: 0 transition: opacity 2s , transform 0.5s @media ( max-width : 1039px ) transform: translateY( 100px ) @media ( min-width : 1040px ) transform: translateY( 200px ) &.active opacity: 1 transform: translateY( 0 ) </style> lazy-boxはVue.jsのコンポーネントの slot機能 を用いることで任意のコンテンツに対してアニメーションを付与することが可能です。 D3.jsを用いたグラフ Recruitページ のグラフはD3.jsを使って実装しています。D3.jsはデータに基づいてドキュメントを操作するライブラリで、データ可視化によく用いられます。今回は、棒グラフとその数値のアニメーションの実装に用いました。 具体的には次に示すようなメソッドをVueコンポーネントのmethodsに設定し、コンポーネントがactiveになったタイミングで各メソッドを呼ぶようにしています。 methods: { barTransition() { var selection = d3.select( this .$refs.bar); selection .transition() .ease(d3 [this .ease ] ) .duration( this .duration) .styleTween( "width" , () => { var i = d3.interpolate(0, '100%' ); return function (t) { return i(t); } ; } ); } , valueTransition() { var selection = d3.select( this .$refs.value); var value = this .value; selection .transition() .ease(d3 [this .ease ] ) .duration( this .duration) .tween( "span" , function (d) { var i = d3.interpolate(0, value); var self = this ; return function (t) { d3.select( self ).text(parseInt(i(t))); } ; } ); } } まとめ 今回は、先日行った弊社のコーポレートサイトリニューアルについてその特徴と実装について紹介しました。 レスポンシブ対応をしっかり行ったので、画面サイズの変更やランドスケープモードへの切り替えなど、ぜひ色々試しながらVASILYのことを今一度知っていただけると幸いです。 最後に VASILYでは、積極的に挑戦していけるエンジニアを募集しています。興味のある方は以下のリンクからぜひご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
アバター
こんにちは。データチームの後藤です。 弊社のデータサイエンティストは職務の1つとしてファッション×機械学習の研究・開発に取り組んでいます。このファッション×機械学習の分野は世界中の大学や研究機関で精力的に研究されているため、我々も最新の動向を日々追いかけて、技術検証やサービスへの実用化を進めています。 本記事では、ファッション×機械学習の最新の研究動向を理解するための比較的新しい研究論文を紹介します。この記事を読むとファッション×機械学習の応用例を把握することができると思います。特に注目している研究の紹介には論文中の図とコメントを残しましたので、追いかける際の参考にしてください。なお、本記事内に掲載されている論文の中にはarXivのみに投稿されているものもあります。「査読を通しておらず内容が保証されない」「今後バージョンアップされ内容が変更される」といった可能性があります。ご了承ください。 一言でファッション×機械学習といっても、その内容は多岐にわたります。今回はファッションに関する行動のうち、 流行を知る 商品を探す アイテムの組み合わせを考える というタスクを機械学習システムで補助するという状況で利用できそうな研究を紹介します。 流行を知る Fashion Forward: Forecasting Visual Style in Fashion (2017) StreetStyle: Exploring world-wide clothing styles from millions of photos (2017) Changing Fashion Cultures (2017) その他関連研究 商品を探す 推薦システム VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback(2015) Sherlock: Sparse Hierarchical Embeddings for Visually-aware One-class Collaborative Filtering(2016) その他関連研究 画像検索システム Cross-domain Image Retrieval with a Dual Attribute-aware Ranking Network(2015) Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce(2017) Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search (2017) その他関連研究 アイテムの組み合わせを考える Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data (2017) Trip Outfits Advisor: Location-Oriented Clothing Recommendation(2017) その他関連研究 最後に 参考 流行を知る 「現在のファッショントレンドのうち、流行り続けるのはどれか」「かつて流行っていたスタイルは今後ふたたび流行するか?」といったファッショントレンドの未来予測は、ファッション業界のデザイナーや販売業者、製造業者にとって重要なタスクです。大抵の消費者は、今後廃れていくスタイルの服よりも、今後主流になっていく服を購入したいと考えるでしょう。 トップダウン式に流行が決まると言われているファッションにおいて、データドリブンなファッショントレンドの未来予測は可能なのでしょうか。過去の傾向をデータで定量化する研究はいくつかありますが、未来を予測する研究は意外にも少ないようです。 Fashion Forward: Forecasting Visual Style in Fashion (2017) この研究 1 では、複数の機械学習タスクをうまく組み合わせることによって、データドリブンなファッショントレンドの予測に成功しました。 この研究のポイントは、 属性予測タスクを学習させたConvolutional Neural Networks(以下、CNNと呼ぶ)による特徴量抽出 Non-Negative Matrix Factorizationによるスタイル情報の抽出&画像データのグルーピング シンプルなExponential smoothing modelによる購買頻度の予測 ファッショントレンドの未来予測に効くのは、タグやテキストのメタ情報よりも画像情報 です。 上図の青線は各スタイルの服がAmazonで購入される頻度の、年ごとの推移を表したものです。緑の線が論文で提案されている手法での予測結果、その他の線は比較用のモデルの予測結果を表します。ほとんどの場合において、論文で提案された手法が実際の結果に近い値を予測していることがわかります。 StreetStyle: Exploring world-wide clothing styles from millions of photos (2017) 上記の研究と同様に、この研究 2 でもファッショントレンドの定量化をCNNによる特徴量抽出と、クラスタリングによって行っています。この研究は、Instagramから地域別・時期別に人の画像が集められ、分析に用いられている点が特徴です。 画像特徴量のクラスタリングにより、抽出されたファッションスタイルの時間変化やクラスター内の地域別の内訳を調べることができます。上図の左は、各クラスターに含まれる画像の例、真ん中がクラスター内の平均画像、右がクラスター内のデータの月別・地域別の分布を表します。 上図の3行目では、黄色と黒色のストライプ柄のユニフォームを着ているクラスターの人たちの投稿が、特定の時期・地域で増えていることがわかります。これはワールドカップが開催された影響だと考えられています。 この研究ではデータの前処理を丁寧に行っています。各画像から人物を検出し、顔の位置を特定して画像のスケールを合わせています。さらに、Amazon Mechanical Turkをつかい、各画像に12種類の属性を付与しています。研究で用いられたデータはSTREETSTYLE-27Kというデータセットとして公開され利用できるようになります。 Changing Fashion Cultures (2017) この研究 3 では、世界のファッショントレンドを分析できるポテンシャルを秘めている巨大なデータセットを提供しています。 上の表から、データ数の点で他を圧倒していることがわかります。DeepFashion 4 や上述のSTREETSTYLE-27のような細かな属性付与はないようなので、その点はうまく処理する必要がありそうです。しかし、データの量の点で困ることはしばらくないでしょう。 その他関連研究 Who are the Devils Wearing Prada in New York City? 5 ニューヨークのファッションショーが消費者のファッションに与える影響について定量化 Fashion Conversation Data on Instagram 6 InstagramのFashionに関わるImageとconversation dataを提供 marketingに有効なInstagramの投稿の特徴量を評価 上述の研究から、画像データをトレンド分析で使える形に定量化する手法や、トレンド情報を含むデータセットが揃ってきたことがわかります。弊社では、タグやテキストベースのトレンドの可視化を行ってきましたが、より高度な分析をする際にこれらの手法が参考になると考えます。 商品を探す ここでは、ECサイトで商品を探すという状況を想定しています。多くのECサイトでは、ユーザーがクエリを入力して主体的に商品を探す検索システムと、行動ログに基づいて商品の表示を変化させる推薦システムが活用されています。ファッションのドメインにおいてはテキスト情報やメタ情報よりも見た目の情報が重要になる場合が多く、画像特徴量を使う研究が盛んです。 推薦システム 推薦システムはユーザーに新しい商品との出会いを促す手助けをしてくれます。ユーザーが付けたアイテムに対するレーティング情報やクリックログを元に構築されることが多いです。しかし、ビジュアルが重要な役割をもつファッションのドメインにおいては、商品の見た目の情報がユーザーの行動に強い影響を与えていると考えられます。また、見た目の情報を活用するにしても推薦システムは計算時間も膨大になりがちなので、学習データの量に対してスケーラブルであることも求められます。ここでは、見た目の情報を使いながらも現実的な計算量に収めて推薦を実現している研究を選びました。 VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback(2015) この研究 7 はBayesian Personalized Ranking(以下、BPRと呼ぶ)に商品の見た目の情報を加えて推薦の精度を向上させています。 (論文中でFはCNNの出力の次元、KがItem Latent Factorsの次元となっているので、図中の「F×1」は「K×1」の間違いだと考えられます) この研究のポイントは、 商品の見た目の特徴量を学習済みCNNを使って抽出 計算オーダーはLatent factorの次元KとVisual latent factorの次元Dに対して線形 ユーザーの評価が少ないコールドスタートなアイテムの推薦に強いことを示唆 です。 論文中の実験結果から、服に関するデータセット(Amazon Women、Amazon Men、Tradesy.com)に関してBPRに対しVBPRのAUCが大きく向上していることがわかります。一方で、携帯電話に関するデータセット(Amazon Phones)の推薦ではAUCの大きな伸びは見られませんでした。このことは、ファッション商品の推薦において見た目の情報を取り入れることの大切さを示しています。 Sherlock: Sparse Hierarchical Embeddings for Visually-aware One-class Collaborative Filtering(2016) この研究 8 では、上述のVBPRと同じ著者が、見た目の情報に商品カテゴリの階層構造を加えることでさらに精度を向上させています。 CNNから得た画像特徴量を関数(Embedding matrixと呼ぶ)を使って次元圧縮をし、Visual latent factorとして推薦に使うところは上記のVBPRと同じです。この研究ではそれに加えてEmbedding matrixにカテゴリの階層情報の制約を与えて学習させます。 このような構造を取り入れることで、全アイテムに共通する要素(明度や彩度など)の特徴量が扱えるようになっています。上図は、学習されたEmbedding matrixの各次元(D0、D1、D8、D9)で順位付けされたアイテムを表しています。D0、D1はTop-levelのカテゴリに対応付けられる次元、D8、D9はBottom-levelのカテゴリに対応付けられる次元です。D0、D1は商品の明度や彩度などのグローバルな特徴と反応し、D8、D9は商品のフォーマルさなどの細かな特徴と反応していることがわかります。VBPRに比べ精度も向上し、計算量も大幅には増えないため、有効な改良の1つと言えるでしょう。 弊社でもファッションアイテムの推薦に見た目の情報を取り入れる実験・検証は進められており、TECH BLOGでも紹介しています。 tech.vasily.jp その他関連研究 Exploiting both Vertical and Horizontal Dimensions of Feature Hierarchy for Effective Recommendation(2017) 9 構造の扱いを親子関係だけでなく同じ階層の関係(兄弟・従兄弟関係)にも拡張したモデル 見た目の情報が取り入れられるようになったMatrix Factorizationベースのモデルも、まだ改良の余地が残されています。例えば、移り変わるトレンドの情報に対応する、個人の好きなブランドの情報を補助情報を扱えるように改良するなどです。より様々な文脈で活用できる推薦システムを構築することが今後の目標です。 画像検索システム 画像検索システムには単純に見た目が似ている画像を返すものの他に、 写り方や見た目が異なる同一商品を返す検索(Cross-domain Image Search) 画像の他に属性情報をクエリに加えて検索結果を柔軟に変化させる検索(Cross-modal Image Search) もあります。特に前者の、ファッションモデルの着こなしの画像や個人が撮影した写真をクエリにECサイトの商品画像を検索するという需要は高いです。 ここでは、実際にImage Searchを実装する上で使えそうな工夫を行っている論文を選びました。 Cross-domain Image Retrieval with a Dual Attribute-aware Ranking Network(2015) Cross-domain Image Searchの研究は数多くありますが、この研究 10 は検索精度を上げるための重要な工夫をしています。 ドメイン固有の表現を学習させるために,ショップ画像とストリート画像のネットワークは分けて学習させる トリプレット損失関数によるランキング学習だけでなく、複数の属性の予測問題を木構造的に学習 背景の影響を小さくするために、R-CNNによる検出を活用 などです。 上の表は、各ネットワークの検索精度の評価です。ANが属性予測タスクを学習したネットワーク、ARNがANに加えてランキングも学習したネットワークを表しています。著者らの提案したDARNはドメイン毎にサブネットワークを使い分け、ドメイン固有の表現を学習したネットワークです。この中ではDARNが最もよい性能を発揮しており、ドメイン毎にネットワークを分ける工夫が検索精度に効くことが示唆されています。 ただし、パラメータの多いCNNを2つも使っている点で学習のコストが上がるというデメリットもありそうです。 Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce(2017) インドのECサイトFlipkartの取り組み 11 もとても参考になります。 この研究のポイントは、 ディープなCNNと並列にShallow Layersを加え、low-levelな特徴量(色や模様など)を使っている点 区別の難しいin-clsss negativeをつかってトリプレットを構成し、微妙なニュアンスの違いを学習させている点(下図) です。 通常のトリプレットでは、クエリ画像に対して同じクラスに属するpositiveと、他のクラスに属するnegativeを組にして学習に用いますが、この研究では、同じクラスに属していながら見た目が微妙に異なるin-class negativeもnegativeとして学習させています。定量的な比較は述べられていませんが、著者等によると、細かな違いに対する感度が上がるようです。 Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search (2017) 「見た目はこんな雰囲気なんだけど、色は青でふわふわな襟のものが欲しい」というちょっとわがままなクエリに答えるシステムも研究 12 されています。この研究では、以下の図のように、服の色や形の属性を操作して画像検索の結果を変化させることができるシステムを構築しています。 属性をインプットするモジュールでは、属性(色、丈、素材、形など)の特徴量ベクトルを、その属性を持つ実際の画像の特徴量の平均値で初期化し保持しておきます。画像と属性のインプットに対して、保持しておいた情報を画像に加え、属性変換後の画像特徴量としてトリプレット損失関数を計算します。 この研究とネットワークのアーキテクチャは大きく異なりますが、弊社でも同様のタスクを研究しIBIS2016で発表しました。VAEとGANの生成モデルを活用した属性変換による画像検索システムです。興味のある方は読んでみてください。 tech.vasily.jp その他関連研究 Visual Discovery at Pinterest(2017) 13 画像ブックマークサービスのPinterestにおける画像検索・推薦エンジンの紹介 Visual Search at eBay(2017) 14 eBayでの画像検索の実装例 アイテムの組み合わせを考える 明日どんな服を着ようか、と考える際、様々な要素(ドレスコード、流行、季節、天気、色の調和、動きやすさなど)を考慮し手持ちの服を組み合わせます。そんな複雑な意思決定を機械学習システムで補助するのがOutfit Support Systemです。(装い一式のことを英語ではOutfitといい、日本でよく使われる「コーディネート」という表現はしないそうです。以下ではコーディネートのことをOutfitと表記します。) 通常、ファッションアイテムの組み合わせの提案は雑誌のコンテンツやショップの店員さんなど、おしゃれ上級者によってなされます。そして、組み合わせの良し悪しはとても繊細で、人によって評価が分かれるなど主観に左右されることもあります。このような複雑な判断を機械学習システムにやらせようとすると「組み合わせのモデリングの難しさ」と「ファッションのコンセプトの捉えにくさ」に直面します。 ここでは、評価の難しいOutfitを上手く定量化している研究を選びました。 Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data (2017) この研究 15 で提案されているシステムは、ユーザーがすでに組み合わせているアイテムに対して、マッチするアイテムを教えてくれます。このシステムでは以下の図のようにデータを扱い学習します。上段ではアイテムの組み合わせからOutfitの質を評価する手順を、下段では1つ1つのファッションアイテムを特徴量に変換する手順を示しています。 この研究のポイントは、 組み合わされるアイテムの数が可変であることから、Outfitの入力をRecurrent Neural Networks(RNN)を使って評価 スタイルやブランドなどの文脈情報を考慮するために、画像、カテゴリ、タイトルのmulti-modalなデータを同時に利用 End-to-Endでの最適化 です。 この研究ではPolyvore.comのOutfitのデータを使っていますが、弊社の運用するIQONに投稿されたデータも同等の情報をもっているため、類似のシステムを構築することが可能だと考えています。 Trip Outfits Advisor: Location-Oriented Clothing Recommendation(2017) 旅行先を入力すると旅行先に合ったOutfitを推薦してくれるシステムの研究 16 です。 この研究のポイントは、 観光地の文脈も考慮(中国のビーチでは保守的な人が多いため、ビキニよりもロンパースを提案する、お寺ではスカートを提案しない) メジャーな観光地を背景とした際に映える色の組み合わせを提案 です。 アイテムの組み合わせの良し悪しやロケーションとの関係について研究が進みつつあります。実際の購買行動に結びつける場合はパーソナライズが必要であると考えます。観点としては、 好み 体型 手持ちの商品 予算 などが挙げられます。 その他関連研究 Intelligent fashion styling using genetic search and neural classification (2015) 17 標準的でない体型の女性に対するOutfitの提案システム GetDressed: A Personalized and Contextually-Aware Clothing Advisor for the Home (2014) 18 手持ちのアイテムからOutfitを決めるシステム 上述の観点をすべて盛り込んだ専属スタイリストの役割をシステムで実現することは非常に難しい問題です。しかし、上述のOutfit自体の評価方法やロケーションとOutfitの関係の研究が揃ってきたので、それぞれの手法を統合したより柔軟なシステムの構築が可能だと考えます。 最後に 今回は、ファッションに関する行動のうち「流行を知る」「商品を探す」「アイテムの組み合わせを考える」というタスクを補助する機械学習システムの研究を紹介しました。ファッションに関するデータに機械学習を組み合わせることにより、人の判断を助ける様々なシステムが実現可能であることがわかっていただけたかと思います。 今回紹介できませんでしたが、ほかにも Object Detection Human Parsing Attribute Prediction Style Understanding などファッション×機械学習の興味深い様々な分野があります。 今年の10月にはコンピュータビジョンの国際会議ICCV2017がイタリア開催されます。この会議では"Computer Vision for Fashion"なるファッションをテーマにしたワークショップが開かれる予定です。ファッション×機械学習の研究分野はさらなる盛り上がりを見せる勢いです。 VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 参考 Z. Al-Halah, R. Stiefelhagen, K. Grauman. Fashion Forward: Forecasting Visual Style in Fashion. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1705.06394.pdf ↩ K. Matzen, K. Bala, N. Snavely. StreetStyle: Exploring world-wide clothing styles from millions of photos. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1706.01869.pdf ↩ K. Abe, T. Suzuki, S. Ueta, A. Nakamura, Y. Satoh, H. Kataoka. Changing Fashion Cultures. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1703.07920.pdf ↩ Z. Liu, P. Luo, S. Qiu, X. Wang, X. Tang. Deepfashion: Powering robust clothes recognition and retrieval with rich annotations. In Proc. CVPR, 2016. Retrieved from http://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Liu_DeepFashion_Powering_Robust_CVPR_2016_paper.pdf ↩ K. Chen, K. Chen, P. Cong, W. H. Hsu, J. Luo. 2015. Who are the devils wearing prada in new york city? In Proc. ICM, 2015. Retrieved from https://arxiv.org/pdf/1508.04785.pdf ↩ YI. Ha, S. Kwon, M. Cha, J. Joo. Fashion Conversation Data on Instagram. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1704.04137.pdf ↩ R. He, J. McAuley. VBPR: Visual Bayesian Personalized Ranking from Implicit Feedback. In Proc. AAAI, 2016. Retrieved from https://arxiv.org/pdf/1510.01784.pdf ↩ R. He, C. Lin, J. Wang, J. McAuley. Sherlock: sparse hierarchical embeddings for visually-aware one-class collaborative filtering. arXiv, 2016. Retrieved from https://arxiv.org/pdf/1604.05813.pdf ↩ Z. Sun, J. Yang, J. Zhang, A. Bozzon. Exploiting both Vertical and Horizontal Dimensions of Feature Hierarchy for Effective Recommendation. In Proc. AAAI, 2017. Retrieved from https://aaai.org/ocs/index.php/AAAI/AAAI17/paper/view/14679 ↩ J. Huang, RS. Feris, Q. Chen, S. Yan. Cross-domain image retrieval with a dual attribute-aware ranking network. In Proc. ICCV, 2015. Retrieved from http://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Huang_Cross-Domain_Image_Retrieval_ICCV_2015_paper.pdf ↩ D. Shankar, S. Narumanchi, H. A. Ananya, P.Kompalli, K. Chaudhury. Deep Learning based Large Scale Visual Recommendation and Search for E-Commerce. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1703.02344.pdf ↩ B. Zhao, J. Feng, X. Wu, S. Yan. Memory-Augmented Attribute Manipulation Networks for Interactive Fashion Search. In Proc. CVPR, 2017. Retrieved from http://www.zhaobo.me/papers/cvpr_atman.pdf ↩ A. Zhai, D. Kislyuk, Y. Jing, M. Feng, E. Tzeng, J. Donahue, Y. L. Du, T. Darrell. Visual discovery at pinterest. In Proc. IWWWC, 2017. Retrieved from https://arxiv.org/pdf/1702.04680.pdf ↩ F.Yang, A.Kale, Y.Bubnov, L.Stein, Q.Wang, H.Kiapour, R. Piramuthu. Visual Search at eBay. arXiv, 2017. Retrieved from https://arxiv.org/pdf/1706.03154.pdf ↩ Y. Li, L. Cao, J. Zhu, J. Luo. Mining Fashion Outfit Composition Using An End-to-End Deep Learning Approach on Set Data. IEEE Transactions on Multimedia, 2017. Retrieved from https://arxiv.org/pdf/1608.03016.pdf ↩ X. Zhang, J. Jia, K. Gao, Y. Zhang, D. Zhang, J. Li, Q. Tian. Trip Outfits Advisor: Location-Oriented Clothing Recommendation. IEEE Transactions on Multimedia, 2017. Retrieved from http://ieeexplore.ieee.org/abstract/document/7907314/ ↩ A. Vuruskan, T. Ince, E. Bulgun, C. Guzelis. Intelligent fashion styling using genetic search and neural classification. International Journal of Clothing Science and Technology, 2015. Retrieved from https://www.researchgate.net/profile/Cueneyt_Guezelis/publication/275257326_Intelligent_fashion_styling_using_genetic_search_and_neural_classification/links/56e45fb708ae68afa11061a5.pdf ↩ Z. Liu, J. Suarez, Y. Wu, F. Yu. GetDressed: A Personalized and Contextually-Aware Clothing Advisor for the Home. Retrieved July 21, 2017, https://static1.squarespace.com/static/5357036ee4b04cbb6439b475/t/54697fd6e4b0bb15f1889fda/1416200150924/GetDressed_FinalPaper.pdf ↩
アバター
こんにちは、バックエンドエンジニアの塩崎です。 先日、会社の広報のためのインターン生紹介記事にメンターとして掲載していただきました。 大学四年生のインターン生と一緒に写真撮影を行ったのですが、見た目だけではどちらが年上かわからなかったので、「メンターの塩崎(右)」という表記をされてしまいました(笑) インターンでも実際のサービスに触れ、課題を解決!〜VASILY DEVELOPERS BLOGが公開されました〜 さて、VASILYではData WarehouseとしてGoogle BigQuery(BigQuery)を利用しています。 BigQuery内にはプロダクトのマスタデータとユーザーの行動ログが格納されています。 そして、それらに対する横断的なクエリを発行することでプロダクトの成長のためのKPIをモニタリングしています。 そのためAmazon Relational Database Service(RDS)に保存されているマスタデータをBigQueryに同期する処理を定期的に実行する必要があります。 先日、ワークフローエンジンであるDigdagとバルクデータローダーであるEmbulkを利用して、この処理を行うシステムを構築しました。 本記事ではどのようにしてDigdagとEmbulkを連携させたのかなどの具体的な方法を、設定ファイルを交えながら説明します。 なお、本システムが対象としているRDSのDBエンジンはAuroraですが、MySQLでも問題なく動作します。 EmbulkとDigdagについて Embulk Digdag DigdagとEmbulkを組み合わせる システムの概要 1. 同期対象のテーブル情報の取得 2. Embulkの起動 3. AuroraからBigQueryへの同期 4. 実行ログの保存 Digdag導入の理由 タスクのリトライ処理 並列実行 HA構成 Digdag UIでタスクの実行ログを確認 設定ファイル Digdag aurora_to_bigquery.dig sync_one_table.dig Embulk aurora_to_bigquery.yml.liquid Digdagサーバーの構成 デーモン化 /etc/systemd/system/digdag.service /etc/init.d/digdag.sh /etc/digdag/config.properties Digdag UI デプロイ まとめ 参考リンク EmbulkとDigdagについて まずは本システムを構築するために使用したEmbulkとDigdagについて説明します。 どちらともTreasure DataさんによるOSSであり、ロゴの動物が可愛いです。 Embulk EmbulkはいわゆるETLツールと呼ばれるソフトウェアです。 大量のデータを取り出し(Extract)、それを加工し(Transfer)、書き出す(Load)ことを主目的としたツールです。 EmbulkではMySQLやBigQueryなどの具体的なデータソースとの入出力に関する部分のソフトウェアはプラグインとして提供されております。 そのため、それらを組み合わせることによって様々な入出力に対応できることが特徴です。 Digdag もう一方のDigdagはワークフローエンジンと呼ばれるソフトウェアです。 複数個のタスク間の依存関係からなるワークフローを定義し、そのワークフローの実行及び管理を行うソフトウェアです。 これだけですと、いまいち導入のメリットが分からないかもしれないので、具体例をお見せします。 例えば、処理1を実行した後に、処理2、処理3、処理4を順番に実行するというケースを考えます。 そして、これらの一連の処理(ワークフロー)を1日1回00:00に実行するとします。 この時、cronでこの問題を解決しようとすると、典型的には以下のようになります。 0 0 * * * 処理1 0 10 * * * 処理2 # 処理1は10分以内には終わるはず 0 20 * * * 処理3 # 処理2は10分以内には終わるはず 0 50 * * * 処理4 # 処理3は30分以内には終わるはず ところが、このcronの設定には様々な問題があります。 何かしらの原因によって処理1の実行時間が10分以上かかってしまった時に、処理2以降は実行を待つべき 処理1が失敗した時には処理2、3、4の実行を取りやめるべき この程度の複雑さであれば、以下のように書けば解決できます。 0 0 * * * 処理1 && 処理2 && 処理3 && 処理4 ですが、例えば以下のような複雑な要望があった場合にはcronではお手上げです。 各処理に対して3回までのリトライを行いたい 処理2・処理3・処理4は互いに独立な処理なので並列実行したい 同時に並列実行する数は2に抑えたい Digdagであればこのような場合でも以下に示すようなシンプルな設定ファイルでこれらの要望を叶えることができます。 +task1 : sh> : 処理1 +parallel_tasks : _parallel : true +task2 : _retry : 3 sh> : 処理2 +task3 : _retry : 3 sh> : 処理3 +task4 : _retry : 3 sh> : 処理4 なお、ワークフローエンジンというジャンルのソフトウェアにはJenkins、Airflow、Luigiなどがありますが、Digdagはそれらと比べて以下のメリットを持ちます。 YAMLベースのシンプルな設定ファイルなので学習コストが低い High Availability(HA)構成が容易 分散環境での動作が容易 DigdagとEmbulkを組み合わせる DigdagとEmbulkを組み合わせることによって、Embulkの良さをさらに引き出すことができます。 Embulkはあくまでも1つのETL処理を効率良く行なうためのツールなので、複数個のETL処理の管理はできません。 例えば、Auroraに保存されている複数個のテーブルをすべてBigQueryに転送する場合を考えます。 この時にはテーブル数と同じ回数だけEmbulkを起動する必要があります。 転送途中でエラーが発生してしまっても、リトライ処理や、失敗したことの通知はEmbulkではできません。 さらに、テーブルの同期の前後に任意の処理を挟み込むこともEmbulkではサポートされていません。 Digdagを利用することによって、Embulk複数回起動を効率的に管理したり、ETLの前後に処理を挟み込むことが容易になります。 システムの概要 以下に今回構成したシステムの概要図を示します。 まず、上図で1〜4の番号が書かれた部分を説明します。 設定された時刻(現状では1日1回)になると、Digdagは以下の処理を行います。 1. 同期対象のテーブル情報の取得 同期対象のテーブル情報をAuroraから取得します。 デフォルトではAuroraに存在するすべてのテーブルの同期を行います。 2. Embulkの起動 DigdagがEmbulkを起動します。 複数テーブルの同期処理は互いに独立であるため、処理の高速化のために複数プロセスのEmbulkを起動し、並列処理を行います。 3. AuroraからBigQueryへの同期 EmbulkがAuroraからBigQueryへの同期処理を行います。 AuroraやBigQueryへの接続に必要な情報はDigdagから渡されます。 Digdagから渡された情報はEmbulkに組み込まれているテンプテートエンジンのLiquidによって展開されます。 4. 実行ログの保存 1〜3の各処理の実行ログはS3にアップロードされます。 実行ログにはタスクの開始時刻・終了時刻・標準出力へ出力した内容などが含まれます。 また、タスクが失敗した場合はSlackにそのことが通知されます(上図右上)。 さらに、タスクの実行ログの確認や失敗したタスクのリトライ処理をDigdagに組み込まれているweb UI(Digdag UI)から行うことができます(上図左上)。 Digdag導入の理由 TECH BLOGの以前の記事ではgoラッパーとEmbulkを利用した同期システムを紹介しましたが、今回のシステムを構築するにあたってはgoラッパーは使用しませんでした。 Embulkを利用したデータ転送基盤の構築 このままgoラッパーを拡張しながら利用し続けることと比べて、Digdagを採用することで以下の問題を簡単に解決することができたからです。 タスクのリトライ処理 タスクの1回あたりの成功確率が十分に高かったとしても、そのタスクを連続で行うと、すべてのタスクの成功する確率は低くなります。 例えば、成功確率が99.9%のタスクを1,000回連続で行う場合、それらすべてが成功する確率は36.8%しかありません。 10,000回連続で行う場合では、わずか0.0045%になってしまいます。 しかし、失敗した時にリトライ処理を1回行うだけで、これらの確率はそれぞれ99.9%、99.0%になります。 実際に、今回のようなネットワークアクセスを行うタスク(Aurora、BigQueryなど)はネットワークの不調によってタイムアウトすることがあります。 その影響でタスクの実行に失敗することもあるため、リトライ処理は必須です。 以下のグラフに連続でタスクを実行した時のすべてが成功する確率(縦軸)とタスクの連続回数(横軸)の関係を示します(注:縦横軸は対数軸)。 3回までのリトライを許容すると、10 9 (10億)回連続で実行しても99.9%の確率で全てのタスクが成功します。 このように、リトライ処理は非常に効果的な一方で、実装やテストが面倒なためしばしば忘れがちな処理でもあります。 Digdagではタスク定義の中に1行の設定を書くだけでリトライ処理を行うことができるので、リトライ処理を簡単に導入することができます。 _retry : 3 +task1 : sh> : sometimes_fail.sh +task2 : sh> : sometimes_fail.sh +task3 : sh> : sometimes_fail.sh また、ワークフローの後半部分のタスクが失敗した場合、ワークフローの最初からやり直すのは無駄です。 特にワークフロー前半部分のタスクの処理時間が長い場合には無駄が顕著です。 そのため、失敗したタスクのみのリトライ処理をしたいです。 Digdagではタスク毎にリトライを設定することでこの問題を解決することができます。 +task1 : _retry : 3 sh> : sometimes_fail.sh +task2 : _retry : 3 sh> : sometimes_fail.sh +task3 : _retry : 3 sh> : sometimes_fail.sh 並列実行 互いに独立なタスクを並列実行することによって、ワークフロー全体の実行時間を短くすることができます。 また、それぞれのタスクが使うマシンリソースの種類(CPU、メモリ、ネットワークなど)が違う場合にはマシンリソースの有効活用をすることができます。 しかし、ミスなく並列処理を行うプログラムを書くのは難しく、再現性のないバグと戦うことも少なくありません。 Digdagを使用することによって、この並列処理の面倒な部分をDigdagが肩代わりしてくれるため、Digdag利用者は個々のタスクに集中することができます。 さらに、並列実行数の調整もDigdag起動時のオプションで簡単に行うことができます。 HA構成 ワークフロー管理をcronで行う場合の問題点の1つとしてあげられるのは、そのサーバーが落ちた場合にどうするかということです。 cronサーバーはステートレスなサーバーではないため、簡単にスケールアウトできません。 Digdagはワークフローそのものやワークフローの状態をPostgreSQLに保存することによってこの問題を解決しています。 タスクの実行を行うworkerやDigdag UIは状態をステートレスなため簡単にスケールアウトすることができ、HA構成にすることができます。 もちろんDigdagをHA構成にするときには、RDSをMulti-AZ構成にするなどの方法でPostgreSQL自体もHA構成にする必要があります。 Digdag UIでタスクの実行ログを確認 Digdagサーバーにはweb UI(Digdag UI)が付属しており、これを利用して以下の操作などを行うことができます。 ワークフロー実行ログの確認 失敗したワークフローのリトライ ワークフローを今すぐに実行 これらの操作をDigdag UIから行うことができるため、Digdagのコマンドに詳しくないメンバーでも運用作業を行うことができます。 Digdag UIの起動方法はドキュメントのどこにも書いてありませんが、実はDigdagサーバーを起動した時に立ち上がっています。 デフォルトでは、65432ポートで立ち上がっているので、 http://localhost:65432/ にアクセスすることでDigdag UIを見ることができます。 なお、GitHubのREADMEのRequirementsにはNode.js 7.xと書いてありますが、Digdag UIの開発時以外にはNode.jsは不要です。 設定ファイル それではここからは具体的な設定ファイルを交えながら説明をします。 Digdag aurora_to_bigquery.dig timezone : Asia/Tokyo # Slack通知のためのプラグインの読み込み _export : plugin : repositories : - https://jitpack.io dependencies : - com.github.szyn:digdag-slack:0.1.1 webhook_url : https://hooks.slack.com/services/HOGE workflow_name : aurora_to_bigquery # 認証情報の取得 +get_db_info : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install aws-sdk rb> : Prepare.get_db_info require : 'tasks/prepare' # 同期対象テーブルの取得 +get_tables_to_sync : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install mysql2 rb> : Prepare.get_tables_to_sync require : 'tasks/prepare' # 同期処理 +sync : for_each> : table : ${TABLES} _parallel : true _do : call> : sync_one_table.dig このワークフローは大きく3つの部分に分かれています。 それぞれはget_db_info、get_tables_to_sync、syncという3つのタスクです。 最初のget_db_infoではAuroraに接続するためのパスワードやGoogle Cloud Platform(GCP)に接続するためのJSONキーを取得します。 これらはAmazon Key Management System(KMS)に格納されているため、Ruby版aws-sdkで取得され、Digdagの変数にセットします。 2つ目のget_tables_to_syncでは同期対象のテーブルの一覧を取得します。 その内部では、 SHOW TABLES を実行し、テーブルの一覧を取得しています。 そして最後のsyncが同期処理の本体で、内部ではEmbulkを呼び出しています。 上流の2つのタスクによって設定された変数を使い、複数個のテーブルの同期処理を行います。 テーブル毎にパラメーターを変更しながらEmbulkを実行するために call オペレーターを利用しています。 また、 _parallel: true が設定されているため、並列処理が行われます。 同時に並列実行される数はDigdagサーバーを起動する時の --max-task-threads オプションによって制御されています。 そのため、すべてのテーブルの同期処理が同時に実行されることはなく、適切にスレッドプールが使われます。 これらのタスクの実行する環境を隔離するために、Dockerを利用しています。 これによって、Digdagが動いているサーバーそのものにRubyやgemなどを入れる必要がなくなり、サーバー構築の手間を減らすことができます。 自分たちでDockerfileやDockerイメージの管理をするのは煩雑ため、DockerイメージはDockerHubに保存されている公式イメージを利用しています。 そして、それらのイメージの上に必要なライブラリなどをインストールしています。 そのため、初回実行時はgemのインストールのために少々待たされます。 ですが、2回目以降には構築済みのDockerイメージが利用されるため、コンテナが高速に起動します。 sync_one_table.dig +get_timestamp_columns : _retry : 3 _export : docker : image : ruby:2.4.1 build : - gem install mysql2 rb> : ColumnOption.get_timestamp_columns require : 'tasks/column_option' +get_columns_to_drop : _export : docker : image : ruby:2.4.1 rb> : ColumnOption.get_columns_to_drop require : 'tasks/column_option' +run_embulk : _retry : 1 _export : EMBULK_INPUT_TABLE : ${table} EMBULK_OUTPUT_TABLE : ${table} docker : image : java:7-jre build : - curl --create-dirs -o /bin/embulk -L https://dl.bintray.com/embulk/maven/embulk-0.8.25.jar - chmod +x /bin/embulk sh> : embulk gem install embulk-input-mysql embulk-filter-column embulk-output-bigquery && embulk run aurora_to_bigquery.yml.liquid _error : slack> : failed-to-sync-table-template.yml このサブワークフローでテーブル毎の設定を取得し、それに基づきEmbulkを起動します。 このワークフローは前述したaurora_to_bigqueryからcallオペレーターによって呼び出されます。 このワークフローはget_timestamp_columns、get_columns_to_drop、run_embulkからなります。 前半の2つのタスクでテーブル毎の固有の情報を環境変数にセットし、最後のタスクrun_embulkでEmbulkを起動します。 get_timestamp_columnsではDATETIME型のカラム名を環境変数に設定しています。 デフォルトのまま同期するとAuroraでDATETIME型のカラムはBigQueryではTIMESTAMP型になります。 Auroraでの日付型がBigQueryでも日付型になるので一見すると問題ないようにも思えます。 しかし、BigQueryのweb UIでクエリの実行結果を確認する時に表示がUTCになってしない、脳内で9時間ずらすのがやや煩雑です。 そのため、 %Y-%m-%d %H:%M:%S%:z というフォーマットのSTRING型で保存することによって見やすさと日付計算のしやすさのバランスを取っています。 このフォーマットであれば通常はJSTで表示されるため人間に確認がしやすく、また、TIMESTAMP型への変換も容易であるため日付計算を行いやすいです。 ちなみに、内部的には以下のSQLを発行し、テーブルのスキーマ情報を読みだしています。 SELECT COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ( SELECT database()) AND TABLE_NAME = '#{table_name}' get_columns_to_dropでは同期対象外のカラムを環境変数に設定します。 本システムで扱うデータにはセンシティブなデータも含まれるため、それらのカラムを同期対象から外します。 どのカラムを除外するのかはYAMLによって制御されています。 この処理のためには embulk-filter-column プラグインを利用しました。 最後のrun_embulkタスクによってEmbulkが呼び出されます。 同期処理のために必要なEmbulk gemのインストールを行った後に、Embulkを起動してテーブルの同期処理を行います。 この処理の最中にエラーが発生した場合はSlackに通知が行われます。 Embulkに引数として渡している aurora_to_bigquery.yml.liquid ファイルについての詳細は次節で説明します。 Embulk aurora_to_bigquery.yml.liquid in : type : mysql host : {{ env.EMBULK_INPUT_DB_HOST }} user : {{ env.EMBULK_INPUT_DB_USER }} password : {{ env.EMBULK_INPUT_DB_PASS }} database : {{ env.EMBULK_INPUT_DB_DATABASE }} table : {{ env.EMBULK_INPUT_TABLE }} { % if env.EMBULK_FILTER_DROP_COLUMN != '' % } filters : - type : column drop_columns : { % assign drop_columns = env.EMBULK_FILTER_DROP_COLUMN | split : ',' % } { % for column in drop_columns % } - { name : {{ column }} } { % endfor % } { % endif % } out : type : bigquery mode : replace auth_method : json_key json_keyfile : content : | {{ env.EMBULK_OUTPUT_JSON_KEY_CONTENT }} project : {{ env.EMBULK_OUTPUT_PROJECT }} dataset : {{ env.EMBULK_OUTPUT_DATASET }} table : {{ env.EMBULK_OUTPUT_TABLE }} open_timeout_sec : 300 send_timeout_sec : 300 read_timeout_sec : 300 retries : 5 gcs_bucket : {{ env.EMBULK_OUTPUT_GCS_BUCKET }} auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" column_options : { % assign columns = env.EMBULK_OUTPUT_TIMESTAMP_COLUMNS | split : ',' % } { % for column in columns % } - { name : {{ column }} , type : STRING, timestamp_format : "%Y-%m-%d %H:%M:%S%:z" } { % endfor % } このファイルがEmbulkに渡す設定ファイルです。 Embulkに組み込まれたテンプレートエンジンであるLiquidを使用しています。 shオペレータを使ってコマンドを実行すると、Digdagの変数が環境変数にセットされます。 そのため、Embulk内では、env.<環境変数名>という記法でそれを読み出すことができます。 環境変数にはスカラー型の情報しか格納できないため、配列として格納したい時にはカンマ区切りで格納して、それをsplitしています。 また、以下の環境変数はKMSに保存されており、上流のget_db_infoタスクによってセットされた状態でEmbulkが起動されます。 EMBULK_INPUT_DB_PASS EMBULK_OUTPUT_JSON_KEY_CONTENT 認証情報をYAMLやソースコードに直に埋め込む必要がなくなったため、良いアイデアかと思いましたが、web UIでこの認証情報が丸見えになってしまいます。 なので、何かしらの対策が今後の課題として残っています。 secret機能を利用することで、ログに認証情報が平文で残ることがなくなるので、この機能の利用を検討しています。 Digdagサーバーの構成 デーモン化 Digdagサーバーはsystemdを利用してデーモン化を行っています。 デーモン化に必要なファイルについては、以下のgistを参考にしました。 https://gist.github.com/uu59/23968e3983adcf5b4cce400710b51cb2 ワークフローやタスクの状態を管理するDBにはH2DBを利用し、DigdagサーバーのローカルディレクトリにDBファイルを配置します。 今回の構成ではDigdagサーバーは1台だけなので、local-agentとexecutor-loopの両方を有効にします。 ワークフローのソースコードやタスクの実行ログはS3に保存します。 S3にアクセスするための認証情報はIAMインスタンスプロファイルを利用するために、設定ファイルには書き込んでいません。 /etc/systemd/system/digdag.service [Unit] Description=digdag [Service] User=root Restart=always TimeoutStartSec=30s Type=simple ExecStart=/etc/init.d/digdag.sh [Install] WantedBy=multi-user.target /etc/init.d/digdag.sh #!/usr/bin/env bash set -ue exec >> /var/log/digdag.log exec 2>> /var/log/digdag-error.log exec /usr/local/bin/digdag server --database /var/digdag/database --access-log /var/digdag/log/access_log --config /etc/digdag/config.properties /etc/digdag/config.properties server.bind=127.0.0.1 server.port=65432 database.type=h2 archive.type=s3 archive.s3.bucket=<s3 bucket name> archive.s3.path=digdag/archive log-server.type=s3 log-server.s3.bucket=<s3 bucket name> log-server.s3.path=digdag/log Digdag UI Digdag UIに対してVPCの外側からアクセスをするためにElastic Load Balancer(ELB)を利用しました。 今回のケースですとDigdagサーバーが1台しかないため、ELBを負荷分散のためには使用していません。 しかし、DigdagがHA構成を簡単に構築することを考慮し、HA構成に変更するときのためにELBの下にDigdagを紐付けました。 また、Digdag UIにアクセスするとワークフローに関するほぼすべての操作を行うことができます。 そのために、Digdag UIにアクセスできるユーザーを制限する必要があります。 現状では、ELBで接続元のIPアドレスを見てアクセス制限を行っています。 デプロイ ワークフローのデプロイはCircleCIから行います。 GitHubでのmasterブランチに対するmergeをhookし、CircleCI上のコンテナから digdag push コマンドを発行します。 まとめ DigdagとEmbulkを併用することでAuroraに保存されているマスタデータをBigQueryに同期するシステムを構築することに成功しました。 Embulk単体では使いづらい部分がいくつかありましたが、Digdagがその部分を補うことによってシステムとしての完成度を高めることができました。 特に、リトライ処理機能、並列実行の機能、web UIによる実行ログの確認機能は自力で書くのが大変なため、Digdagがこれらの面倒を見てくれることに大変助かっています。 Digdagに関する資料は少ないので、問題があった時にStack OverflowやQiitaを見ても情報が全く見つからない時もあります。 そのため、Digdagのソースコードを読んで問題の解決を行うこともしばしばありました。 VASILYではこのような困難に挑戦しながらも新技術を積極的に導入したい仲間を大募集中しています。 興味のある方は以下のバナーからご応募ください。 参考リンク Digdag公式HP Digdagの公式HPです。 Digdagの機能に関するビデオを見ると、一通りの機能について知ることができます。 Digdag公式ドキュメント Digdag公式ドキュメントです。 まだまだドキュメントに書かれていない部分も多いので、詳しい挙動が気になったときにはソースコードを読むのが良いでしょう。 Embulk公式HP Embulkの公式HPです。 Embulkは公式HPとドキュメントページが共通です。 digdag-slack Slackに通知を行うためのDigdagプラグインです。 httpオペレーターを使ってSlackに通知するのと比べると、より柔軟な通知が可能です。 embulk-input-mysql MySQLからデータを読みだすためのEmbulkプラグインです。 Auroraからデータを読みだすことにも使えます。 embulk-output-bigquery BigQueryにデータを書き出すためのEmbulkプラグインです。 一旦Google Cloud Strage(GCS)にデータを転送し、そこからBigQueryにロードすることで、処理の安定化・高速化を行うことができます。 embulk-filter-column 特定のカラムに対する処理を行うためのEmbulkプラグインです。 今回は特定のカラムを削除するために使用しましたが、それ以外の用途にも使うことができます。
アバター
この記事ではOpenAPI Specification v2に関する内容を取り上げています。しかし、2023年9月現在での最新の仕様はOpenAPI Specification v3となっています。最新の仕様に基づいて実装や学習を行いたい方は、公式ドキュメントやそれに関連する資料をご参照ください。 こんにちは! バックエンドエンジニアのりほやんです。 以前、テックブログでAPIモックと仕様書を作成することができるSwaggerについてご紹介しました。 Swaggerそのものについて知りたい場合やSwaggerを実際に導入したい場合は、こちらの記事がとても参考になります。 techblog.zozo.com 本記事では、SwaggerのAPI定義を行うSwagger YAMLの記法についてまとめてみました。 使い初めはとっつきにくいSwaggerですが、この記事がSwaggerを使う方の参考になれば幸いです。 目次 目次 Swagger Editorの紹介 初級編 基本の記述 swagger info paths パスのURL HTTPメソッド(get, post, put, delete等) エントリポイントのリクエストとレスポンスに関する記述 parameters responses スキーマオブジェクトについて JSONオブジェクト 配列 中級編 Swaggerを構成するオブジェクト definitionsを使う definitionsを複数呼び出す 上級編 definitionsを入れ子にする Swagger YAMLを書く上で気をつけたい点 レスポンスの配列に複数データを入れたい formatを指定しても、デフォルトで値が入らない Amazon API Gatewayではexampleが使えない まとめ 参考 Swagger Editorの紹介 Swagger YAMLを書く際には、 Swagger Editor がとても便利です。 画面左側がエディター、右側がSwagger UIとなっておりリアルタイムで記法のチェックや定義書を確認できます。 Swaggerを書くエディタはいろいろありますが、気軽に記法を試す際にはSwagger Editorがとても便利です。 ぜひこれから紹介する記法を試す際にも、ぜひ使ってみてください。 初級編 基本の記述 初めにSwagger YAMLを記述するにあたり必須であるswagger, info, pathsについて説明します。 上記の基本的な構成で記述したシンプルなSwagger YAMLがこちらです。 swagger : "2.0" info : description : "これはペットストアに関するAPIです。" version : "1.0.0" title : "Petstore API" termsOfService : "http://swagger.io/terms/" contact : email : "apiteam@swagger.io" license : name : "Apache 2.0" url : "http://www.apache.org/licenses/LICENSE-2.0.html" paths : /pet/{petId} : get : summary : "ペット情報API" description : "指定されたpetIdの情報を返します" parameters : - name : "petId" in : "path" description : "取得したいペットのID" required : true type : "integer" format : "int64" responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" 以降、このサンプルを基に記法を説明します。 実際に上記のSwagger YAMLをSwagger Editorに入力すると、画面右側にAPI定義書が下記のように作成されます。 それでは、 swagger, info, pathsそれぞれのオブジェクトの書き方について説明します。 swagger swaggerには、Swaggerのバージョンを記述します。 変更する理由がない限りはここの値は 2.0 にしておきます。 info タイトル・説明・バージョンなど、APIについての情報を記載します。 infoには、以下の情報を記述できます。 フィールド名 型 説明 必須 version string APIのバージョン 必須 title string ドキュメントのタイトル。一番上に表示される。 必須 description string ドキュメントについての説明 termsOfService string 利用規約 contact contact object APIについての問い合わせ先 license license object APIのライセンス paths APIのエントリポイントを記述します。 サンプルでは下記の部分にあたります。 ここで定義する情報をもとにエントリポイントが作成されるため、pathsはとても重要なパートとなります。 pathsには、下記のような階層形式で情報を記載します。 パスのURL(/pet/{PetId}) HTTPメソッド(get, postなど) エントリポイントのリクエストとレスポンスに関する記述 パスのURL 実際に定義したいエントリポイントのパスを記述します。 サンプルでいうところの /pet/{petId} です。 HTTPメソッド(get, post, put, delete等) パスの下には、パスのHTTPメソッドを記述します。 下記のように複数記述することもできます。 /pet/{petId} : get : summary : "ペット情報API" description : "指定されたpetIdの情報を返します" parameters : 省略 responses : 省略  delete : summary : "ペット情報API" description : "指定されたpetIdを削除します" parameters : 省略 responses : 省略 エントリポイントのリクエストとレスポンスに関する記述 HTTPメソッドの下層には、エントリポイントがどのようなリクエストを受け取り、どのようなレスポンスを返すかを記述します。 サンプルでは、この部分にあたります。 設定できるフィールドは以下になります。 フィールド名 型 説明 必須 responses response object 返ってくるレスポンス 必須 parameters parameter object リクエストのパラメーター tags array swaggerオブジェクトで定義するどのtagに紐付けたいかを記述 summary string エントリポイントの概要(120文字以内) description string エントリポイントの説明 consumes/produces array MIME Type schemes array APIの通信プロトコル。必ずhttp, https, ws, wssの4種類のどれかを記述する。 security security requirement object 適用するセキュリティ externalDocs external docs document 外部リンク deprecated boolean Deprecatedかどうかをtrueかfalseで記述。デフォルトはfalse。 もっとも重要なparametersとresponsesについて補足説明します。 parameters リクエストの際に渡すパラメーターを記述します。 サンプルでは下記の部分にあたります。 記述するフィールドは以下です。 フィールド名 型 説明 必須 name string パラメーター名 必須 in string パラメータの場所。query, header, path, formDataの4種類のどれかを記述してください 必須 description string パラメータの説明 required boolean 必須パラメーターかどうかをtrueかfalseで記述 schema schema object bodyのパラメーターをスキーマオブジェクトとして記述。スキーマオブジェクトについては後述。inがbodyである場合に使用。 inがbodyである場合、必須 type string パラメーターのタイプ。 必ずstring, number, integer, boolean, array, fileの中から選ぶ。inがbody以外である場合に使用。 inがbody以外である場合、必須 format string パラメーターの型。 こちら から選ぶ。inがbody以外である場合に使用。 typeとformatの指定は、 こちら が参考になります。 パラメーターが記述されると定義書ではこのように表示されます。 responses 返ってくるレスポンスを記述します。 サンプルでは下記の部分にあたります。 返したいHTTPステータスコードごとに、定義を行います。 定義できる設定は下記です。 フィールド名 型 説明 必須 description string レスポンスの説明 必須 schema schema object レスポンスのbody。スキーマオブジェクトで記述する。スキーマオブジェクトについては後述。 headers headers object レスポンスヘッダーを記述 example example object レスポンス例。レスポンスの値を自分で定義したいときに用いる。 レスポンスを設定すると定義書が下記のように生成されます。 スキーマオブジェクトについて parameters, responsesを記述する際に、schemaを記述することができます。 このschemaにはスキーマオブジェクトを記述します。 スキーマオブジェクトは、bodyに用いるデータのタイプを定義することができるオブジェクトです。 主に配列かJSONオブジェクトを表現するときに使います。 JSONオブジェクト schema : type : object と指定すると、JSONオブジェクトを返すことができます。 例えば、APIのレスポンスを下記のように返したいとします。 { " id ": 1 , " name ": " doggie " } この場合はschemaをこのように書きます。 schema : type : object properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" type: object を指定した場合は、propertiesを設定します。 propertiesでは、カラムの情報を記述します。 propertiesには基本的に下記3つが記述されていれば動作します。 フィールド名 型 説明 type string パラメーターのタイプ。必ずstring, number, integer, boolean, array, fileの中から選ぶ。 format string パラメーターの型。 example typeで選んだ型 レスポンスで返したい文言 typeとformatの指定は、 こちら が参考になります。 配列 schema : type : array と指定すると、配列を定義することができます。 例えば、APIのレスポンスを下記のように返したいとします。 [ { " id ": 1 , " name ": " doggie " } ] この場合はschemaをこのように書きます。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" type: array を指定した場合は、itemsを設定します。 itemsには、配列の中のオブジェクトを記述します。 配列の中に、JSONを定義したい場合は、 type:object を指定します。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" また、配列の中に文字列を定義したい場合は、 type: string を記述します。 schema : type : array items : type : "string" 中級編 Swaggerを構成するオブジェクト Swaggerは初級編で紹介したswagger, info, pathsも合わせ計15種類のオブジェクトから成り立っています。 多く感じられますが、すべてが必須というわけでなく、必須であるswagger, info, pathsが記載されていれば動きます。 必須以外のものは定義書に記載したいものがあれば記述します。 フィールド名 型 説明 必須 swagger string swaggerのバージョン。特に指定がなければ、デフォルトの2.0のままで大丈夫です。 必須 info info object タイトル・説明・バージョンなど、APIについての情報を記載します。 必須 paths paths object 提供するAPIのパスを書いていきます。Swagger定義の要です。 必須 host string API通信を行うサーバーのホスト。無記載であればドキュメントが動いているサーバーのホストとなります。 basePath string ホストに続くパス。スラッシュ(/)から始まる必要があります。無記載の場合、ホストの直下になります。 hostを example.com basePathを /v2 とした場合、APIのパスは http://example.com/v2/〜 となります。 schemes array APIの通信プロトコル。必ずhttp, https, ws, wssの4種類のどれかを記載してください。無記載の場合、Swaggerの定義書がアクセスしているスキームになります。 produces array APIが提供できるMIME Typeの指定。選択肢は こちら consumes array APIが使用するMIME Typeの指定。選択肢は こちら definitions definitions object レスポンスやパラメータに使用するデータ定義を記載します。 parameters parameters definitions object パラメーターを定義します。pathsの下に直接パラメーターを書くこともできますが、複数回同じパラメーターを使う場合に便利です。 responses responses definitions object レスポンスを定義します。pathsの下に直接レスポンスを書くこともできますが、複数回同じレスポンスを使う場合に便利です。 securityDefinitions security definitions object セキュリティに関する定義を行えます。Oath認証についてもこちらで定義します。 security security requirement object securityDefinitionsにて定義したセキュリティの中で何を適用するかを指定します。 tags [ tag object ] タグの名前と説明を定義します。定義したタグをpathに紐付けることで、タグごとにpathがまとまりドキュメントが見やすくなります。 externalDocs external documentation object 外部リンクを定義します。ドキュメントに貼りたいリンクがある場合、記載することでリンクが作成されます。 それぞれのオブジェクトの記述は、 Swagger Editor にデフォルトで入っているPet store APIがわかりやすく参考になります。 definitionsを使う 同じスキーマオブジェクトを複数回使用したい場合、definitionsを使用しテンプレートとして定義することができます。 definitionsを使う際のポイントは、下記のようになります。 テンプレート化したいスキーマオブジェクトを、definitionsに定義をする 呼び出したい箇所に、$refを使用して、definitionsのオブジェクトを呼び出す サンプルをdefinitionsを用いて書き換えると以下のようになります。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : $ref : "#/definitions/Pet" # definitionsで定義されたスキーマオブジェクトを呼び出す definitions : Pet : # テンプレート名 type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" definitionsは本当に便利です。 definitionsにレスポンスのスキーマを全て定義しpathsではそれらを呼び出すだけというような記述にすると、Swagger YAMLが見やすくなるのでオススメです。 definitionsを複数呼び出す definitionsにて定義したスキーマオブジェクトを複数呼び出すことも可能です。 下記のように、Storeの情報をdefinitionsで定義して呼び出してみます。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : pet : $ref : "#/definitions/Pet" store : $ref : "#/definitions/Store" definitions : Pet : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" Store : type : "object" properties : id : type : "integer" format : "int64" example : 1 store_name : type : "string" example : "ABC PET STORE" レスポンスはこのようになります。 { " pet ": { " id ": 0 , " name ": " doggie " } , " store ": { " id ": 1 , " store_name ": " ABC PET STORE " } } schema : type : "object" 上記のように記述し、JSONオブジェクトのそれぞれのキーにdefinitionsで定義したスキーマオブジェクトを呼び出します。 上級編 definitionsを入れ子にする 中級編では、レスポンスにdefinitionsで定義したスキーマオブジェクトを呼び出しました。 definitionsはスキーマオブジェクトを定義し、呼び出すことができるのでschemaが使えるところではどこでも呼び出すことができます。 例えば、definitionsの中でスキーマオブジェクトを呼び出すことも可能です。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : $ref : "#/definitions/Pet" definitions : Pet : type : "object" properties : id : type : "integer" format : "int64" name : type : "string" example : "doggie" stores : type : "array" items : $ref : "#/definitions/Store" # Storeを呼び出す Store : type : "object" properties : id : type : "integer" format : "int64" example : 1 store_name : type : "string" example : "ABC PET STORE" 上記のようにレスポンスを定義した場合、レスポンスはこのように生成されます。 { " id ": 0 , " name ": " doggie ", " stores ": [ { " id ": 1 , " store_name ": " ABC PET STORE " } ] } storesの配列の中で、itemsの1つとしてStoreオブジェクトを呼び出しています。 Swagger YAMLを書く上で気をつけたい点 ここからは、実際に私がSwagger YAMLを書く上でうまくいかなかったりつまづいた点を共有の意味を込めてご紹介します。 レスポンスの配列に複数データを入れたい 通常レスポンスに配列を定義する際は、このように記述します。 schema : type : array items : type : "object" properties : id : type : "integer" format : "int64" example : 1 name : type : "string" example : "doggie" この記述で返ってくるレスポンスは [ { id: 1, name: "doggie" } ] と要素が1つしか返ってきません。 2つ以上の配列要素を追加したい場合は、オブジェクト自体のexampleを記述します。 paths : /pet/{petId} : get : 省略 responses : 200 : description : "成功時のレスポンス" schema : type : "object" properties : pet : $ref : "#/definitions/Pet" definitions : Pet : type : "array" items : example : - id : 1 name : "doggie" - id : 2 name : "pochi" formatを指定しても、デフォルトで値が入らない Swaggerのドキュメント には、スキーマオブジェクトのformatにemailやuuidを指定するとexampleを設定しなくても値が入るという記述があります。 実際、Swagger Editorでは下記のようにデフォルトで値が入ります。 しかし、Swagger Codegenを使ってモックアプリケーションを生成する場合には、formatを指定してもデフォルトで値が入りません。 formatのデフォルト値を使用する際は、ご自分の環境でデフォルト値が入るかどうかを確認してから使う方が良いと思います。 Amazon API Gatewayではexampleが使えない Swaggerを用いてAPIを生成するサービスの1つに、 Amazon API Gateway があります。 API Gatewayは、Swagger YAMLをインポートすることで簡単にAPIを作成することができます。 API Gatewayを用いてAPIを作成する方法については、下記の2つが参考になるのでぜひ読んでみてください。 チュートリアル: サンプルをインポートして REST API を作成する - Amazon API Gateway http://tech.vasily.jp/entry/swagger_api_mock API GatewayにSwagger YAMLをインポートしてモックアプリケーションを作成する場合は、exampleが使えません。 exampleが記述されているSwagger YAMLをAPI Gatewayでロードすると Invalid model schema specified というエラーが返ってきます。 API Gatewayでレスポンス例を記述したい時は、下記のように総合レスポンスの設定時に記述しましょう。 まとめ 以上、基本的なSwagger YAMLの書き方についてご紹介しました! Swaggerは高機能であるゆえに、最初はとてもとっつきにくく感じてしまいます。 しかし一度覚えてしまえば、Swaggerは開発効率を上げることができるとても便利なフレームワークです。 この記事では紹介しきれなかった機能も多くあるので、ぜひ こちら のドキュメントを参考にしてみてください。 VASILYでは、一緒にバックエンド開発をする仲間を大募集しています♪ ご興味のある方は、こちらからご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com 参考 petstore.yaml Swagger Specification
アバター
こんにちは。バックエンドエンジニアインターンの田島です。 VASILYでは分析にBigQueryを使用しており、MySQLのデータを毎日BigQueryに同期しています。この同期処理を行うシステムは、約2年前にRubyで書かれたもので、プロダクトの成長に伴うデータ量の増加に耐えることができなくなり始めていました。そのため、同期処理を行うシステムを一から作り直しましたので、その詳細についてご紹介します。 弊社DEVELOPERS BLOGでは以前、『 インターン生がデータ転送基盤を一から設計する、VASILYバックエンドインターンの紹介 』としてシステムの概要・開発の流れをご紹介しましたが、今回はシステムの詳細についてご紹介します。 (photoクレジット *1 ) データ同期ツールの紹介 新たなデータ同期システムとして、次のように利用する社内ツールを作成しました。 以下のような環境変数を設定し、コマンドを実行するだけで、指定したデータベースの全てのテーブルがMySQLからBigQueryに同期されます。 export DB_HOST= DBホスト名 export DB_USER= DBユーザー名 export DB_PASS= DBパスワード export DB_DATABASE= DBデータベース名 export GCP_KEY= GCPのkeyfileへのパス $ mysql_to_bigquery また、以下のように引数を渡すことで、同期したいテーブルだけを指定することも可能です。 $ mysql_to_bigquery --tables tablename1,tablename2 システム構成 構成は以下のようになっています。 (ファイル・YAMLファイル・ほうれん草の画像クレジット *2 ) 実装詳細 Embulkについて 今回、データ同期処理に Embulk を利用しました。Embulkとは、ログ収集でよく利用されるfluentdのバッチ版のようなツールです。データベースやストレージからデータを吸い出し、別のデータベースやストレージにロードできます。YAML形式で設定ファイルを書くことで、その設定を元にEmbulkがデータを同期します。 Embulkプラグイン Embulkではプラグインが採用されており、今回の同期のシステムでは以下の2種類のプラグインを利用しています。 embulk-input-mysql embulk-output-bigquery embulk-input-mysql embulk-input-mysqlはembulk-input-jdbcの1つです。embulk-input-mysqlを利用することで、MySQLからのデータの吸い出しが可能です。以下のように設定ファイルに記述することで、テーブルごとに出力することができます。optionsを設定することで、jdbcオプションを渡すことができます。 in : type : mysql host : ホスト名 user : ユーザー名 password : パスワード database : 対象のデータベース table : 対象のテーブル select : "*" options : { useLegacyDatetimeCode : false , serverTimezone : Asia/Tokyo } 今回利用したオプションの {useLegacyDatetimeCode: false, serverTimezone: Asia/Tokyo} はTime Zoneに関するオプションです。今回のシステムでは、Embulkが動作するサーバーのTime ZoneがAsia/Tokyoとなっています。上記オプションを付けない場合、EmbulkがMySQLのTime ZoneをUTCであると判断してしまい、9時間ずれた状態でTimeStamp型のデータを取得してしまいます。 このオプションについては、以下の記事の中で教えて頂きました。Embulkコミュニティは親切な人が多いのが特徴です。 embulk-input-jdbcのMySQLプラグインで9時間時間がずれる embulk-output-bigquery embulk-output-bigqueryを利用することで、BigQueryへのデータ出力が可能です。以下のように設定ファイルを記述することで、任意の場所に吸い出したデータを格納できます。以下のように gcs_bucket を設定することで、吸い出したデータを一度GCSに格納します。これにより、BigQueryへのinsert処理の高速化とjob数の節約ができます。 out : type : bigquery mode : replace auth_method : json_key json_keyfile : json_keyfileへのパス project : プロジェクト名 dataset : データセット名 table : テーブル名 gcs_bucket : GCSのバケット名 auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" Goラッパー 今回のデータ同期システムを実装する上で、Embulkだけでは実現できないことがありました。そこでGoラッパーを作成しました。Goラッパーを作成する主な目的として以下が挙げられます。 Goラッパーを作成する目的 設定ファイル生成とEmbulkの実行 logの管理と通知 設定ファイル生成 Goラッパーを作成する1つ目の目的として設定ファイルの生成が挙げられます。Embulkでは1つのテーブルにつき1つの設定ファイルを作成する必要があります。同期するべきテーブルが複数あるので、全ての設定ファイルを手で作成することは現実的ではありません。そこで、Goラッパーによる設定ファイルの自動生成を行うことにしました。 設定ファイル生成と実行の流れ MySQLからスキーマの情報を取得 得られた情報から設定ファイルを生成 各設定ファイルを元に、Embulkを実行 MySQLからスキーマの情報を取得 複数のテーブル同期するために、最初にMySQLのスキーマ情報を取得する必要があります。そこで、 SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ( SELECT database()) というSQLを発行し、テーブル名、カラム名、カラム型を取得します。この情報を利用し、 {name: '' , colomns: [{name: '' , type : '' }]} というデータ構造を作成し保持します。 得られた情報からYAMLファイルを生成 Embulkの設定ファイルにはテンプレートエンジンの"Liquid"がデフォルトで利用できるようになっています。しかし、今回はGoとの相性を考え、Goのテンプレートエンジンである"pongo2"を利用しました。"pongo2"は"jinja"ライクなテンプレートエンジンで以下のように利用しています。 in : type : mysql host : {{ host }} user : {{ user }} password : {{ password }} database : {{ database }} table : {{ table }} select : "*" options : { useLegacyDatetimeCode : false , serverTimezone : Asia/Tokyo } out : type : bigquery mode : replace auth_method : json_key json_keyfile : json_keyfile_path project : project_name dataset : dataset_name table : {{ table }} gcs_bucket : gcs_bucket_name auto_create_gcs_bucket : false compression : GZIP source_format : NEWLINE_DELIMITED_JSON default_timezone : "Asia/Tokyo" 上記のテンプレートにMySQLから取得したスキーマ情報をそれぞれ流します。これにより得られた設定をtable_name.ymlとして保存することで、各テーブルの同期設定ファイルを作成します。 各YAMLファイルを元に、Embulkを実行 最後に、作成された設定ファイルを元にEmbulkを実行することで、テーブルが同期されます。 ログ収集と通知 Goラッパーの2つ目の目的としてはログの収集と、通知処理が挙げられます。Embulkだけでは、柔軟なログ収集とエラー発生時の通知ができないので、Go側で管理する必要があります。 弊社ではエラーの収集にhorensoを利用しています。horensoについては弊社DEVELOPERS BLOG『 horensoで作るモダンなcronスクリプト監視環境 』で紹介しています。horensoに対して適切にエラー発生を通知するには、stderrにログを出力する必要があります。Embulkではエラー発生時のログも合わせて、全てstdoutに出力されるようになっています。その為、Go側でEmbulkの終了ステータスを利用し、適切にログの出力先を変える必要があります。 まとめ Embulkを利用することで同期処理が安定し、プロダクトの成長を支えるデータ同期基盤を構築することに成功しました。また、Goラッパー作成により、運用の効率化に成功しました。Embulkはまだまだ発展中のプロジェクトでプラグインも増え続けています。Embulkのコミュニティは親切な人が多いので、困ったことがあっても素早く解決することができます。ぜひ、Embulkを利用してみてください。 最後に このシステムは、短期インターン時に一から作ったものです。VASILYのインターンでは、このような実践的・挑戦的な課題に挑むことができます。 サマーインターンへのご応募をお待ちしております!! https://www.wantedly.com/projects/103184 www.wantedly.com *1 : photo: Christopher Michel *2 : File icon: Icons made by Freepik from www.flaticon.com is licensed by Flaticon Basic License YML icon: Icons made by Yannick from www.flaticon.com is licensed by CC 3.0 BY Salad icon: Icons made by Madebyoliver from www.flaticon.com is licensed by Flaticon Basic License
アバター
こんにちは、フロントエンドエンジニアの茨木です。 VASILYでは通期でエンジニアインターンを募集しており、厳しい選考を突破した学生をインターン生として受け入れています。中にはインターン後選考に進んで内定を獲得するケースもあります。フロントエンドチームでも3月に来たT君が短期インターンを経てめでたく内定しました。本記事では、彼の事例をご紹介したいと思います。サマーインターンを考えている方々の参考になれば幸いです。 学生について T君は電気系の学科で勉強している大学4年生です。フロントエンドの経験はインターン直前までほとんど無かったそうですが、独学で弊社のフロントエンドインターン課題を突破してきました。この課題はテーマに沿ってサービスを開発するものですが、要件を満たすだけでなく開発効率化やスクレイピングへのチャレンジを行っていたのが印象的でした。 課題 T君には短期インターンの課題として、弊社が運営するIQONのAMP対応に取り組んでもらいました。AMPはGoogleなどが推進しているモバイルページ高速化の仕組みで、AMP対応により検索エンジンから来るユーザーのユーザビリティが向上します。AMP対応ではガイドラインに沿った専用ページを新たに作成する必要があります。IQONには多くのコンテンツがありますが、T君には店舗ページ・コーディネートページのAMP対応をやってもらいました。 店舗ページ ( https://www.iqon.jp/store/%E6%9D%B1%E4%BA%AC%EF%BC%88%E6%B8%8B%E8%B0%B7%EF%BC%89/19370/ ) コーディネートページ ( https://www.iqon.jp/sets/3823957/ ) 日程 今回は以下のような日程でインターンに取り組んでもらいました。 1日目 アカウント準備や開発環境の構築を済ませてから、肩慣らしで簡単な改修に取り組んでもらいました。弊社のフロントエンドインターンは初日デプロイがしきたりになっていますが、彼も無事に初日デプロイを成し遂げました。 2日目 2日目からは店舗ページのAMP対応に取り組んでもらいました。AMPページには通常ページと同じコンテンツを掲載する必要があるので、Google Mapsの地図もAMPページに掲載する必要があります。Google MapsをAMPページに埋め込むケースは初めてだったので、まず導入方法を調査してもらいました。AMP関係の公式ドキュメントはほとんど英語なのですが、彼は問題なく読みこなしていました。 3〜5日目 調査が無事完了したので実装に取り掛かってもらいました。実装完了後にはプルリクエストを出してもらい、コードレビューを行いました。チームでの開発経験が少なかった彼は20件程の指摘にやや驚いていましたが、全てしっかり対応してくれました。コードレビュー完了後はデザイナーやディレクターにも確認してもらう必要があるので、その確認依頼も彼にやってもらいました。確認完了後にデプロイ作業を行い、めでたく店舗ページのAMP対応が完了しました。 6〜8日目 店舗ページのAMP対応が早めに完了したので、コーディネートページのAMP対応をやってもらいました。 9〜10日目 VASILYのインターンでは最終日に役員に向けた成果発表があります。9日目からはそれに向けて資料作成や練習をしてもらいました。学生ではなかなか経験しない発表準備に苦労している部分もありましたが、エンジニアではない役員もいる中で全員にしっかり伝わる発表ができました。 成果 店舗ページ、コーディネートページのAMP対応が完了しました。通常のスマートフォン用ページと比べても遜色のない出来になっており、既存の使い勝手を損なうことなく検索エンジンから来るユーザーのユーザビリティを改善しました。 通常の店舗ページ AMP版店舗ページ 本人の感想 10日間のインターンはとても楽しくあっという間に過ぎていきました。 今回の課題であったAMPページの実装は、比較的新しく、これからより機能が充実して普及していく技術だと思うのでとてもいい経験になりました。 社員の皆さんはとても陽気で仕事熱心だったので、私も楽しく全力で仕事に取り組むことができました。 今回メンターをして頂いた茨木さんは、私をとても丁寧にサポートしてくださり感謝してもしきれません。 この10日間のインターンの経験を活かして、エンジニアとしてより一層精進し、茨木さんを超えて一流のエンジニアになりたいです! まとめ VASILYのインターンの特徴は社員と同様のタスクに取り組んでもらい、リリースまで責任をもってやりきってもらうことです。これは生半可なことではないので、メンターも気を引き締めてしっかりサポートしています。 もうすぐ本格的に夏が到来しますが、VASILYではサマーインターンを募集しています。やりがいや学びが多いチャレンジングなインターンであること間違いなしですので、我こそはという情熱のある学生のご応募をお待ちしております! https://www.wantedly.com/projects/103184
アバター
初めまして、データチームの上月です。 今回はVASILYテックブログ初の論文紹介、テーマは 自己回帰型モデル(Autoregressive, AR)です。 はじめに VASILYではIQONの類似画像検索にAutoencoderを適用しています。 具体的にはアイテム画像で学習したAutoencoderの潜在変数を特徴量として類似画像検索を行っていますが、背景やモデルの影響を受けやすいなどの課題があります。 この問題は「潜在変数にどのような情報を持たせるか」を調整することで解決できる可能性が高く、潜在変数の分布にはdecoderの表現力が関係しています。 最近ではVAEのdecoderとしてRNNや自己回帰型モデルなどといった表現力のあるモデルを用いたり 1 、 decoderの後にPixelCNN 2 を追加することで潜在変数がもつ情報を目的にあったものにする例があります(PixelVAE 3 )。 そこで今回はARの中でも特に、画像と相性が良いといわれている畳み込み層がメインの 自己回帰型モデルを幾つか紹介します。 はじめに生成モデルとしてメジャーなVAE 4 とGAN 5 について触れた後にARを紹介します。 VAEとGAN 生成モデルでは、データ を生成する分布 を学習することが目的になります。潜在変数 を仮定して が与えられた元での の確率 を学習するのがVAEとGANです。 VAE VAEでは潜在変数の分布を正規分布か一様分布に仮定し、 からEncoderが潜在変数 の分布の統計量 を抽出し、それらを用いて を抽出してDecoderが を生成します。 このナイーブな方法では確率的勾配降下法(以後、SGD)で直接最適化できないのでreparameterization trickを用います。 ここではEncoderが出力した と 、乱数 を用いて、潜在変数を で表すことで end-to-endの最適化が可能になります。 一方で、適切に最適化されても潜在変数の情報がほとんど無視されることがあり、optimization challengesと言われています(詳細はVLAE, InfoVAEを参照)。 下図は乱数から生成される画像の一例です。 GAN GANは をGeneratorでモデリングするためにGeneratorとDiscriminatorの2つのモデルを用いて学習します。この時、Discriminatorは与えられたデータが データセットに含まれるデータか、Generatorが生成したデータかを識別します。ナイーブなGANではDiscriminatorの予測に対する交差エントロピーを誤差として用いますが、最近はWasserstein GAN 6 やCramer GAN 7 をはじめとした確率分布間の尺度を用いて学習の安定化を図る例が増えています。 GAN自体はmin-max gameであり、学習が不安定ですが生成されるデータはVAEに比べてよりリアルなものになります。 GAN全体の目的関数はJS divergenceと関係があります。 VAEは変分下界で評価する一方、GANでは評価基準が定まっておらず、 画像の見た目でモデルを評価することが多いのが1つの欠点です。 自己回帰型モデル 自己回帰型モデルでは、VAEやGANではできなかった直接的な対数尤度最大化が可能です。 番目のデータはそれまでに生成された から 番目のデータ全てに依存すると仮定して学習を行います。 そのためVAEやGANに比べると推論が遅いという欠点があり、例えばPixelCNN++では32x32の画像16枚からなるミニバッチを 生成するのに11分かかります 8 。 画像のcontextについて 1. 生成する順序 2. 同一チャネルでのピクセル間の依存関係 3. チャネル間の依存関係 mask Aは最初に用いるmaskで同一チャネルの情報の利用を認めないマスク、mask Bは同一チャネルの情報の利用を許すマスクです。 ここでは、以下の自己回帰型モデルを紹介します。 MADE DRAW PixelRNN, PixelCNN SketchRNN MADE Germain, M., Ca, M. G., Gregor, K., Com, K. G., Deepmind, G., Murray, I., … Larochelle, H. (n.d.). MADE: Masked Autoencoder for Distribution Estimation. Retrieved from https://arxiv.org/pdf/1502.03509.pdf 課題 従来の自己回帰型モデルの深層学習はシンプルなものより 倍の計算が必要だった 解決策・優位性 マスクを適用することでモデルのアーキテクチャ・計算方法を変えずにARにした 新規性 全結合層のみからなるautoencoderを自己回帰型モデルにするためのマスクを提案 計算量削減 各層の全ユニットに を割り当てて依存関係を管理 概要 3層のMADE 3層のAutoencoderで入力層・隠れ層を結ぶ重みを 、隠れ層から出力層へ結ぶ重みを 、それぞれのマスクを とすると、このマスクを適用したAutoencoderは で表せます。 ここで、マスクは で定義されます。2つのマスクの積 の各要素が と  の依存関係を表しますが、 の要素はすべて になります。 Deep MADE 第 層のユニット数を 、 と 層の結合の重みを とします。各層の各ユニットに を割り当てます。 マスクは と同じ形の行列で表され、 で定義されます。 また、論文では を固定するのではなく、パラメータを更新するたびに をサンプリングすることで精度が上がったことが報告されています。以下、論文に掲載されている生成画像です。 この実験で使われているのはbinarized MNISTで、モデルは全結合層のみですがそれなりの精度でデータが出力されていることがわかります。 DRAW Gregor, K., Danihelka, I., Graves, A., & Wierstra, D. (2014). DRAW: A Recurrent Neural Network For Image Generation. Icml-2015, 1–16. 以下は著者が公開しているDRAWの動作の様子ですが、赤枠で囲まれている注目領域(Attentiion)の遷移がこの手法のポイントです。 www.youtube.com 課題 従来の生成モデルでは1回でデータを生成 画像の全ピクセルが同じ分布に属している状態を想定しているのと等価 そもそも人は書いたり消したりという微修正の反復で絵を生成 解決策・優位性 sequentialなVAEと canvas canvas に各時刻でのdecoderの出力を書き足していく 動的にAttentionの領域を確定 新規性 sequentialなVAEにAttentionを適用 canvasにdecoderの出力を加えることでし、 回に分けて画像を生成 Attention領域の決定には を反映 Attentionを read 関数で動的に決定 一般的なbackpropagationで学習可能 Attentionを用いる学習では強化学習で一般的な方策勾配法を使うことが多い。 概要 一回で画像を生成するのはスケールしにくいという考えから段階的に画像を生成していくモデルです。モデルは下の図のようにsequentialなVAEをアーキテクチャになっており、変分下界の最大化を目的にしています。 実際の目的関数は DRAWでは各イテレーションで画像の一部 だけ に注目(Attention)してデータを生成していきます。上の図からもわかりますが注目領域を決定する際に前のイテレーションでのdecoderの出力を利用します。 次に read と write について説明します。 read 関数によってAttentionの領域を決定します。 read, writeに関して 実際にAttentionはこのように変化していきます。 表記 画像サイズ: ガウシアンフィルタ: グリッド中心: ストライド: 中央値の位置: (i列j行) ガウシアンフィルタの分散: フィルタの出力に対する係数: Attentionに関するパラメータは の線形変換で求めます: これらの値をフィルターバンク でまとめて管理します。 は正則化項です。 read 関数はフィルターバンクを用いて で定義されます。 一方 write では新たに を から定義し、 になります。binarized MNISTでの実験結果では当時ではNLL(負の対数尤度)が最小でAttentionの有無で約7ポイントも変わっています。 PixelRNN・PixelCNN・PixelCNN++ PixelRNN & PixelCNN Oord, A. van den, Kalchbrenner, N., & Kavukcuoglu, K. (2016). Pixel Recurrent Neural Networks. Retrieved from http://arxiv.org/abs/1601.06759 モデル及び高速化の詳細はこちらの スライド を参照ください。 この論文は昨年話題になったDeepMindの WaveNet やNLPの ByteNet などの元となったとも言われています。 論文中では Row LSTM Diagonal BiLSTM PixelCNN の3つが提案されています。 課題 ピクセル間の高次元・広範囲・非線形なピクセル間の関係と、複雑な条件付き分布のモデル化には非常に表現力のあるsequenceモデルが必要 解決策・優位性 2D RNN(LSTM)を用いた12層のモデルを提案 ピクセルの値を256クラス分類問題として定義 ピクセルの値として不適切なものを出力しないようになる 自然画像では0や255がでやすいなどのガウス分布では表現できない多峰性を獲得している 新規性 2D RNN Residual Connections ピクセルの値をチャネル毎に256クラスの分類問題として定義 最終層はsoftmax 混合ガウス分布では表しにくい多峰性 0 ~ 255以外の値を取らない 概要 PixelRNNの2つは最初と最終層でそれぞれmask A、mask Bを適用しますが。PixelCNNではすべての層でmask Aあるいはmask Bを適用します。 PixelRNNは2D LSTMレイヤーでstateに適用し、畳み込みによって特定の次元のstateの計算を一回で行っています。この2D LSTMレイヤーにはRow LSTMとDiagonal BiLSTMの2種類があります。 しかしPixelRNNでは(潜在的には)どんな長さの依存関係も考慮できますが、計算コストが非常に大きいです。この代用として、CNNのみで構成したのがPixelCNNです。PixelCNNでは空間解像度を落とさず、poolingも適用していません。 反映可能な依存関係は小さくなりますが、parallelに学習できるというメリットがあります。 256クラス分類にするメリット 左上はRチャンネルの1番目のピクセルの値の分布を表しているが0と255が多い特徴を捉えていることからも256クラス分るにするメリットが伺えます。 実験・結果 実験でのアーキテクチャは以下です。 画像の補完 画像全体の整合性は取れているように見えます。 対数尤度 発表時では初の70台を達成しましたが、PixelCNN++に抜かれています。 PixelCNNの高速化 PixelCNN++ Salimans, T., Karpathy, A., Chen, X., & Kingma, D. P. (2017). PixelCNN++: Improving the PixelCNN with Discretized Logistic Mixture Likelihood and Other Modifications. Retrieved from http://arxiv.org/abs/1701.05517 課題 PixelRNNの単純化&高速化したものであるPixelCNNでも計算が遅いこと 各チャネル・各ピクセルでの256クラス分類を行っていること メモリ・計算効率が悪い 勾配が非常にスパース 解決策・優位性・新規性 softmaxを利参加したロジスティック回帰に変更 実際に0や255が近傍の値より多いという特徴が現れている 画像全体に対してconditioning dilated convolutionの代わりにダウンサンプリング ダウンサンプリングで失われた情報はショートカットで補完 dropoutによる正則化 その他 PixelCNNの高速化としては - Ramachandran, P., Paine, T. Le, Khorrami, P., Babaeizadeh, M., Chang, S., Zhang, Y., … Huang, T. S. (2017). Fast Generation for Convolutional Autoregressive Models. Retrieved from http://arxiv.org/abs/1704.06001 - Reed, S., Oord, A. van den, Kalchbrenner, N., Colmenarejo, S. G., Wang, Z., Belov, D., & de Freitas, N. (2017). Parallel Multiscale Autoregressive Density Estimation. Retrieved from https://arxiv.org/abs/1703.03664 など他にもあります。 前者はWaveNetを21倍、PixelCNNを183倍高速化に成功、後者はPixelCNNでは の計算量だったが を達成し、8x8から512x512の超解像も成功しています。 Sketch RNN 最後にRNNを用いたSketch RNNを紹介します。 実装は GitHub にあります。 課題 VAE、GANそしてARはピクセル画像が対象となっている ヒトは世界をピクセル画像のようには理解していないし、線だけで描かれた抽象的なスケッチでコミュニケーションをとる 各スケッチのデータは点のリストで表されているのでスケッチを終える判断が困難 スケッチが終わる点の数は少ないためデータに偏りがある 解決策・優位性 手書きスケッチのデータセットからヒトのようなペンの動かし方を学習する スケッチ同士の足し算ができる スケッチの終了判定を克服 新規性 ベクター画像のデータセットを作成・公開 各スケッチのデータは点のリスト(5つの要素)で表されていて、2要素はオフセット、他3要素はその点における描写の状態を表すone-hotベクター。 描写の状態には「描写を終える」も含まれている。 ベクター画像の生成とそれに特化した学習フロー 潜在空間での加減算 概要 seq2seqなVAEでベクター画像を生成するモデルを学習する手法を提案しています。 ベクター画像の場合、与えられるのは点に関するデータのリストなので描写の終了判断が求められ、その判定が難しいですが克服し、 スケッチ画像に特化した学習手法を提案しています。 データセット データセットは点のリストです。 リストの要素、つまりスケッチの各点の情報は5つの要素のベクトル です。ベクトルの初めの2要素は1つ前の点からの距離を表し、残りの3要素はone-hotベクターで各要素は : ペンが紙についていてまだ書き続ける : ペンが紙から離れる : 書く作業が終了していて、この点と後続の点がレンダリングされない ことを表しています。 モデル 図にある通り、seq2seqのVAEをベースにしています。 encoderは双方向で入力は各スケッチを表す点のリストと逆順にしたリストです。 2つのencoderの出力は結合された後に潜在変数 を表すための2つのベクトル に射影されます。 はVAE同様reparameterization trickで と計算されます。 また、decoderでは各 で を予測します。 は 個のガウス分布からなる混合ガウス分布で、 はカテゴリカル分布で をモデルします。 各 でのdecoderの出力 は 個のパラメータを持ちます。 内訳は 個の の二変数ガウス分布のパラメータ とガウス分布の混合重みの 個のパラメータ、そして です。 学習 の分布は非常に偏っているため「どこでスケッチをやめるか」の学習は非常に困難ですが、 この論文ではスケッチのデータセットの中で最も点が多いリストの要素数を として 以下のようにシンプルなロス関数を設計して学習しました。 目的関数はVAE同様にreconstruction lossとKL divergenceの和で表されます。 reconstruction lossは の負の対数尤度と の和で とおきます。 また。KL divergenceの項は潜在変数の次元数を とし、 で表されます。 最終的な目的関数は係数 を用いて となります。 実験・結果 推論時には温度パラメータ を用いて と変形することで終了するタイミングのランダム性を変更できます。 スケッチの生成 潜在変数からの生成 sketchを入力した時の出力 左右の一番下の入力はおそらくデータセットには含まれていませんが出力はデータセットに含まれるような画風に近づけられているように見えます。 潜在変数の足し引き 上段では胴体が書き足され、下段では胴体が除かれているので適切な類推が行われているように見えます。 最後に 今回は自己回帰型モデルを中心に紹介しました。 全体としてまだまだGANやVAEに比べると推論にかかる時間がネックですが対数尤度を直接最適化できるのは大きな魅力ですし 今後高速化の手法が多く出ると信じています。 データチームでは普段からarxivのcs.CVやstat.MLで画像解析・レコメンドの新規手法をチェックし、毎週のTECH ミーティングでは 1週間で読んだ主にファッションに関する論文をエンジニアに紹介しています。 研究成果を実際に課題解決に適用し、サービス改善を行うメンバーを募集しています。 少しでも興味を抱いた方はこちらから応募ください。 Søren, M. F., Sønderby, K., Paquet, U., & Winther, O. (n.d.). Sequential Neural Models with Stochastic Layers. Retrieved from https://arxiv.org/pdf/1605.07571.pdf ↩ Oord, A. van den, Kalchbrenner, N., & Kavukcuoglu, K. (2016). Pixel Recurrent Neural Networks. Retrieved from http://arxiv.org/abs/1601.06759 ↩ Gulrajani, I., Kumar, K., Ahmed, F., Taiga, A. A., Visin, F., Vazquez, D., & Courville, A. (n.d.). PIXELVAE: A LATENT VARIABLE MODEL FOR NATURAL IMAGES. ↩ Kingma, D. P., & Welling, M. (2013). Auto-Encoding Variational Bayes, (Ml), 1–14. Retrieved from http://arxiv.org/abs/1312.6114 ↩ Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., … Bengio, Y. (2014). Generative Adversarial Nets. Advances in Neural Information Processing Systems 27, 2672–2680. Retrieved from http://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf ↩ Arjovsky, M., Chintala, S., & Bottou, L. (2017). Wasserstein GAN. Retrieved from http://arxiv.org/abs/1701.07875 ↩ Bellemare, M. G., Danihelka, I., Dabney, W., Mohamed, S., Lakshminarayanan, B., Hoyer, S., & Munos, R. (n.d.). The Cramer Distance as a Solution to Biased Wasserstein Gradients. ↩ Ramachandran, P., Paine, T. Le, Khorrami, P., Babaeizadeh, M., Chang, S., Zhang, Y., … Huang, T. S. (2017). Fast Generation for Convolutional Autoregressive Models. Retrieved from http://arxiv.org/abs/1704.06001 ↩
アバター
2017年6月7日、第4回目となるFashion Tech meetupを開催しました。今回はVASILYが主催となり、バックエンドチーム、フロントエンドチーム、データチームが業務で行っている開発・運用のノウハウを発表しました。 本記事で弊社の登壇資料を公開しますので、ご参加できなかった方、Fashion Tech meetupを初めて知った方、是非ご一読ください。 メインセッション VASILY流CSSコーディング メインセッションの最初はフロントエンド担当の権守がCSSコーディングについて発表しました。 CSSコーディング規約や運用方法について紹介しています。また、デザインデータを細部まで表現するために使用しているツールや、デザイナーとのやり取りなども紹介しました。中でも大規模リファクタリング時に使用している、弊社で自作したPhantomJSを使用した差分検出ツールに注目が集まっていました。 新サービスの紹介と使用技術について 次にバックエンド担当の北條が新サービスの紹介と主要技術について発表しました。サービスの全体像から、課題とその解決のためのアプローチ方法まで詰まっています。是非スライドをご覧ください。 参加者の方々にはファッション関係の方も多く、名寄せ処理や表記ゆれに関しての共感が得られていました。また、業界問わず必要とされるクローラーについては皆さん気になっていたようです。 新サービスはα版のため、正式公開は後日となりますが、サービスの方も期待して待って頂ければと思います。 ディープラーニングでコーデを提案 最後にデータチームの中村が、ディープラーニングでコーディネートを提案するための実現方法の紹介をしました。ファッション分野では、アイテム画像だけでなく着用イメージが湧くようなスナップ画像もニーズがあります。アイテム画像をクエリとし、スナップ画像を検索する技術の開発をすることで、このニーズを満たすということを行なっています。弊社ではR&Dも行なっており、研究開発した技術はサービスへの導入実績もあります。実験結果つきでスライドを用意していますので、是非ご覧ください。 まとめ 今回はFashion Tech meetup vol.4の様子をお伝えしました。ほんの一部ではありますが、VASILYの技術を共有出来たと思います。また、本記事ではメインセッションの資料紹介となりましたが、イベントではFashion x Technologyを題材したLTや懇親会も行われています。 おわりに 今回は 株式会社リクルートマーケティングパートナーズ に会場提供をして頂きました。素敵なオフィスをお貸しして頂き、ありがとうございました。 会場でVASILYに興味を持った方、資料を見てもっと詳しく知りたいと思った方は、是非オフィスへ遊びにきてください。
アバター
データサイエンティストの中村です。 ファッションアイテムの画像から抽出した特徴量は検索以外にも利用することができます。 今回はレコメンドにおける画像特徴量の活用について、以下の3トピックを考えてみたいと思います。 画像特徴量を利用したコンテンツベースレコメンド モデルベース協調フィルタリングにおけるコールドスタート問題の軽減 画像特徴量を利用したモデルベース協調フィルタリングの高度化 画像特徴量を利用したコンテンツベースレコメンド あるアイテム を好きなユーザーは とよく似たアイテムも好きであると仮定します。このユーザーへの推薦は、 と似ているアイテムを推薦すれば良いわけですが、このとき、アイテムの類似度を評価する尺度として画像特徴量が使えます。 アイテム の画像特徴量を としたとき、アイテム とアイテム の類似度は と の間の距離を用いて表現できます。類似度(距離)にはコサイン類似度やユークリッド距離がよく使われます。 実際にAutoencoderで抽出した特徴量を使ってアイテムを推薦すると、以下のようになります。 コンテンツベースのレコメンドでは、画像特徴量に基づくアイテム類似度を予め計算しておき、直近の履歴に応じて推薦アイテムを決定します。 類似度計算にはトランザクションデータを必要としない為、コールドスタートに強い手法と言えます。 一方で、結果を見て分かる通り、推薦されるアイテムはどれも似通ってしまいます。同じデザインで価格やブランドが違う服を提案したいときには使えますが、新しい発見を得る機会は少なくなります。 モデルベース協調フィルタリングにおけるコールドスタート問題の軽減 モデルベース協調フィルタリングの例として行列分解(Matrix Factorization)があります。 行列分解のインプットはレーティング行列 であるため、レーティングが存在しないアイテムは推薦対象に含めることができません。 この問題は画像特徴量を利用することで軽減することが可能です。 行列分解 行列分解を用いてレーティング行列 を と の積に分解できたとします。このときユーザー とアイテム の相性は 中の対応する要素の内積で表現できます。 (バイアス項は省略しています) はベクトルで、アイテムの特徴量とみなすことができます。 はファクターの数です。 画像特徴量の利用 ファッションECサイトには毎日新しいアイテムが追加されています。レコメンドにもなるべく早い段階で新着アイテムを反映したいという思いがあります。 上の式を参照すると、新着アイテムの特徴量 さえ計算できれば、ユーザーベクトル と内積をとることですべてのユーザーとの相性を計算できそうです。 行列分解の計算後に画像特徴量を利用して新着アイテムの特徴量を無理矢理与えてしまおうというのがここで紹介する方法です。 レーティングが存在するアイテムの集合を 、新着アイテムを で表記します。画像特徴量はレーティングの有無に関係なくすべてのアイテム について計算可能です。新着アイテム の画像特徴量 をクエリに近傍探索を実行すれば、新着アイテムの近傍 を得ることができます。 このとき、新着アイテムの特徴量 を以下のように定義します。 新着アイテムの特徴量 は、近傍のアイテムの特徴量 の重み付き線形和とし、重みは新着アイテムとの類似度に依存する形で定義します。 これにより新着アイテムの特徴量が計算できました。実際に計算された特徴量を可視化すると以下のようになります。 左はクエリ画像、中央は画像が似ているアイテム、右は行列分解で計算した特徴量が似ているアイテムです。クエリ画像はレーティングの付いていない新着アイテムですが、行列分解で計算した特徴量を使って比較しても違和感のないアイテムが並んでいます。 今回は重みを近傍法を利用して定義しましたが、この重みは学習によって求めることも可能です。詳しくはGantner2010 *1 をご覧ください。 画像特徴量を利用したモデルベース協調フィルタリングの高度化 ここでは行列分解のモデル自体を高度化する方法を考えます。 あるユーザーのファッションアイテムに対する好みには、アイテムの見た目も影響を与えていそうです。 この直感を定式化するため、一般的な行列分解の式に画像に依存する項を加えます。 ここで、 はユーザー のアイテム の見た目に対する重み、 はアイテムの特徴量を と同じ次元にマッピングするための行列、 はアイテム の画像自体に対するバイアスです。 右辺第1項だけだと通常の行列分解となります。第2項および第3項が画像に関する項で、第2項はユーザーと画像の相互作用、第3項は画像自体の魅力を表現しています。 これをBayesian Personalized Ranking(BPR)という手法で解いたのがHe2015 *2 です。彼らはこの手法をVisual Bayesian Personalized Ranking(VBPR)と呼んでいます。 実装 VBPRをchainerで実装しました。VBPRのアルゴリズムはシンプルなのでフレームワークを使うまでもありませんが、深層学習フレームワークを利用して実装すれば実績のある最適化アルゴリズムなどの恩恵を受けることができます。 VBPRのモデルの定義は以下のようになります。 import numpy as np import chainer from chainer import functions as F from chainer import links as L from chainer import Variable class VBPR (chainer.Chain): def __init__ (self, n_user, n_item, n_latent, n_visual, d_image, reg= 0.0 ): self.n_user = n_user self.n_item = n_item self.n_latent = n_latent self.n_visual = n_visual self.d_image = d_image self.reg = reg self._layers = { 'latent_u' : L.EmbedID(self.n_user, self.n_latent), 'latent_i' : L.EmbedID(self.n_item, self.n_latent), 'visual_u' : L.EmbedID(self.n_user, self.n_visual), 'visual_i' : L.Linear(self.d_image, self.n_visual, nobias= True ), 'bias_v' : L.Linear(self.d_image, 1 , nobias= True ), } super (VBPR, self).__init__(**self._layers) for param in self.params(): param.data[...] = np.random.uniform(- 0.1 , 0.1 , param.data.shape) def __call__ (self, u, i, j, xi, xj): gamma_u = self.latent_u(u) gamma_i = self.latent_i(i) gamma_j = self.latent_i(j) theta_u = self.visual_u(u) theta_i = self.visual_i(xi) theta_j = self.visual_i(xj) bias_i = self.bias_v(xi) bias_j = self.bias_v(xj) x_uij = F. sum (gamma_u * (gamma_i - gamma_j), axis= 1 ) x_uij += F. sum (theta_u * (theta_i - theta_j), axis= 1 ) x_uij += F.reshape(bias_i - bias_j, (u.shape[ 0 ], )) loss = F.log1p(F.exp(-x_uij)) if self.reg > 0 : loss += self.reg * F. sum (gamma_u*gamma_u, axis= 1 ) loss += self.reg * F. sum (gamma_i*gamma_i, axis= 1 ) loss += self.reg * F. sum (gamma_j*gamma_j, axis= 1 ) loss += self.reg * F. sum (theta_u*theta_u, axis= 1 ) loss += self.reg * F. sum (theta_i*theta_i, axis= 1 ) loss += self.reg * F. sum (theta_j*theta_j, axis= 1 ) loss += self.reg * F. sum (bias_i *bias_i , axis= 1 ) loss += self.reg * F. sum (bias_j *bias_j , axis= 1 ) loss = F. sum (loss) / u.shape[ 0 ] chainer.report({ 'loss' : loss,}, self) 行列 の各行はユーザー/アイテムの分散表現とみなせるので、 EmbedID で定義します。画像特徴量を任意の次元数にマッピングする場合は Linear が使えます。 d_image という変数は画像特徴量の次元数です。 forwardはユーザーID、アイテムIDおよび対応する画像の画像特徴量(ベクトル)を引数に取ります。 実験 参考程度に実験を行いました。実験用に加工したIQONのデータセットを使います。ユーザー数は21382、アイテム数は99337、レーティングの数は547880です。レーティングの値は{0,1}のバイナリになっています。BPR、VBPR、VBPRの第2項と第3項(画像に関する項)(ここではVLRと呼ぶことにします)の3手法をRecall@100とnDCG@100で比べた結果、以下のようになりました。 残念ながら大勝というわけにはいきませんでした。ただし良い感触がまったくないわけではなく、オリジナルのVBPRを再現してパラメータを丁寧に決めればそれなりに差はつくのではないかと考えています。 まとめ レコメンドに画像の情報を反映する方法を紹介しました。 とくに2つ目のコールドスタート対策は簡単に拡張できて使い勝手が良いのでおすすめです。IQONデータセットの場合、この手法を適用することで10万以上のアイテムを推薦候補に加えることができます。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 *1 : Gantner, Z., Drumond, L., Freudenthaler ,C., Rendle ,S., Schmidt-Thieme, L.: Learning Attribute-to-Feature Mappings for Cold-Start Recommendations. In: ICDM. (2010) *2 : R. He., J. McAuley.: VBPR: Visual bayesian personalized ranking from implicit feedback. In: CoRR. (2015)
アバター
こんにちは、Webフロントエンドエンジニアの権守です。 弊社では200以上の提携ECサイトから集めた大量の商品写真を取り扱っています。そのサービスの性質上、画像配信の最適化は非常に重要な課題の1つです。今回は最適化の一環として画像のレスポンシブ対応を導入しましたので、その際に調査した内容やハマったポイントなどを紹介します。 はじめに RetinaディスプレイなどのDPR(Device Pixel Ratio)の高いディスプレイの普及に伴い、Webサービス側でもその対応が必要となっています。基本的には高解像度の画像を用意すればよいですが、高解像度になればその分ファイルサイズは大きくなり、配信にかかる時間も長くなります。そのため、適切な画像を選んで配信する必要があります。 DPRについては こちら のページがわかりやすくまとまっています。 画像の配信制御 ベクター画像 高DPRな環境に対応するにあたり、コーディングという観点から一番簡単な方法はSVGなどのベクター画像を使うことでしょう。 < img src = "logo.svg" > ベクター画像であればいくら拡大しても画像が粗くなるようなことはないので、高DPRでも問題ありません。また、1つの画像をサイズ違いで複数用意し、コーディングするという手間も必要ありません(サイズ違いの画像を用いる場合のコーディングについては後述します)。 一方で、デメリットとして画像解像度が小さい場合にラスタ画像と比べるとファイルサイズが大きくなりやすい点や、写真への適用は難しいという点が挙げられます。 写真などの複雑な画像をベクター画像に変換することは難しく、変換時に画質の劣化やファイルサイズの増大が起こるので現実的に利用することは稀だと思います。ベクター画像への変換が何故難しいかはここでは割愛します。 VASILYでは、多少ファイルサイズが大きくなっても鮮明に表示したいサービスロゴなどの画像に対してSVGを使っています。 アイコンフォント ベクター形式の利用という観点では Font Awesome などのアイコンフォントを使うという手もあります。 アイコンフォントは便利ですが、いくつか欠点もありVASILYでは採用してません。アイコンフォントの欠点については こちら によくまとまっているので、参照するとよいと思います。 多様な画像フォーマットのサポート 高解像度の画像を用意しつつもファイル容量を抑えるのであれば、 WebP や JPEG XR などの新しい画像フォーマットの利用も検討してもよいかもしれません。しかし、これらのフォーマットのサポート状況はブラウザによって異なります。そのため、異なるブラウザのために複数のフォーマットで画像を用意する必要があります。 このような場合にはPicture要素とSource要素を使います。Source要素のtypeにMIMEタイプを指定することで、そのブラウザでサポートされているかどうかを上から順に判定し、最初に見つかった表示可能な画像を表示します。 < picture > < source srcset= "photo1.webp" type = "image/webp" > < source srcset= "photo1.jxr" type = "image/vnd.ms-photo" > < img src = "photo1.jpg" > </ picture > 参考URL 複数の解像度のサポート 表示する環境毎に必要な大きさの画像を選択して表示することで、転送量を抑えつつユーザ体験も保つことが出来ます。 DPR毎に出し分け サイズが固定のレイアウトでRetinaディスプレイなどの高DPRに対応する場合には、srcsetのxデスクリプションを使うとよいです。 < img src = "photo-100.jpg" srcset= "photo-200.jpg 2x, photo-300.jpg 3x" width = "100" height = "100" > 上のようにsrcsetを記述することでDPRの大きさに応じて画像を選択してくれます。srcsetをサポートしていないブラウザではsrcに指定された画像を表示するのでsrcの記述も必須です。 DPRによる画像の切り替えを検証する際には各ブラウザの開発者ツールでDPRを切り替えて確認することができます。Chromeの場合は次のように検証できます。 Retinaディスプレイの端末であってもDPR1の動作を検証できます。 [ サンプルコード ][ サンプル ] 表示サイズ毎に出し分け ウィンドウ幅の80%を横幅とするように画像を表示するといったレイアウトでは、予め画像が表示されるサイズはわかりません。こういった場合にはsrcsetのwデスクリプションを使うとよいです。 < img src = "photo-100.jpg" sizes= "80vw" srcset= "photo-200.jpg 200w, photo-400.jpg 400w, photo-600.jpg 600w, photo-800.jpg 800w" > wデスクリプションを使うことで、ブラウザ側でDPRを考慮した上でsizesから必要な画像の大きさを計算し、srcsetから必要最低限な大きさの画像を選択してくれます。仮にウィンドウサイズが320px、DPRが2だったとした場合の計算例は下のようになります。 必要な解像度 = DPR * 80vw = 2 * 320 * 0.8 = 512 この場合、必要な解像度は512pxなのでそれ以上の解像度を持つ中で一番小さいphoto-600.jpgが表示されます。 * 注意 Chromeの開発者ツールでDPRに1より大きい値を設定した場合に必要な解像度の計算が正しくありません。Androidの実機やiPhoneの実機では問題なく動作しているので、PCのChrome特有のバグかもしれません。 [ サンプルコード ][ サンプル ] また、sizesはその名の通り複数指定することが可能であり、次のように指定すればレイアウトを切り替えることもできます。 < img src = "photo-100.jpg" sizes= "(max-width: 300px) 50vw, 33.3w" srcset= "photo-200.jpg 200w, photo-400.jpg 400w, photo-600.jpg 600w, photo-800.jpg 800w" > この例ではウィンドウ幅が300px以下の場合にはウィンドウ幅の半分のサイズで画像を表示し、そうでない場合にはウィンドウ幅の3分の1の大きさで画像を表示します。このような記述をすることで、横並びのレイアウトであればウィンドウ幅に応じて2カラムの表示を3カラムの表示に切り替えられます。 [ サンプルコード ][ サンプル ] サンプルはDPRに1を設定した状態で確認することをお薦めします。 srcsetの注意点 sizes属性を指定されたimgはsizesから選ばれた大きさになりますが、CSSで明示的に大きさを指定されている場合はCSSで指定された大きさが優先されます。しかし、その場合であってもwデスクリプションの計算に用いられる大きさはsizesのものです。つまり、適切に必要な解像度を求めるためには、CSSでは大きさを指定しないかsizesとCSSで指定するimgの幅は統一する必要があります。 srcsetは同じ画像を複数の解像度で用意することを想定しているため、一度大きな画像をロードをした後にウィンドウを小さくしたとしても、小さい画像を改めて読み込むことはしません。また、同じsrcsetでsizesの小さいimgが同ページ内に存在する場合にもキャッシュされた大きな画像を利用します。 言い換えると、ウィンドウ幅に応じて画像のアスペクト比を切り替えるような指定をした場合に、ウィンドウサイズを小さくしたとしても意図した画像は表示されません。こういった画像の切り替えを行う場合には次に説明するアートディレクションを行う必要があります。 [ サンプルコード ][ サンプル ] サンプルはDPRに1を設定した状態で確認することをお薦めします。 アートディレクション ウィンドウサイズや画面の向きなどの表示している環境に応じて配信する画像の内容を変えることをアートディレクションといいます。例えば、ウィンドウサイズの横幅が大きくなった場合に背景全体も写った画像に切替えることが考えられます。 これをHTMLで表現するにはPicture要素とSource要素を使う必要があります。 < picture > < source media = "(min-width: 45em)" srcset= "large.jpg" > < source media = "(min-width: 32em)" srcset= "med.jpg" > < img src = "small.jpg" alt = "" > </ picture > [ サンプルコード ][ サンプル ] これまで説明した画像配信は組み合わせることも可能です。各組み合わせのサンプルが こちら のページにまとまっているので、合わせて参照されるとよいと思います。 遅延読み込み 画像の配信容量を抑えるテクニックの1つとして遅延読み込みを採用されている方も多いと思います。VASILYでも Lazy Load を用いて遅延読み込みを行っていました。しかし、こちらのライブラリはsrcsetに対応していません。そのため、VASILYではsrcsetの導入に伴い lazysizes を採用しました。採用に至った理由としてはsrcsetに対応していること、jQueryに非依存であることなどが挙げられます。 まとめ 今回は、Webサイトのレスポンシブ対応の中でも特に画像に着目して紹介しました。レスポンシブ対応の導入を検討している方の参考になれば幸いです。 img要素のsizesとsrcsetは HTML5.1 で策定されたものです。今回紹介した技術は比較的新しいものが多く、ブラウザによってはサポートされていないものもあるので、 Can I use などでサポート状況を確認した上での動作検証や導入をお薦めします。 最後に VASILYでは、ユーザー体験をより良くしていけるエンジニアを募集しています。興味がある方は以下のリンクから是非ご応募ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
アバター
こんにちは。バックエンドエンジニアのじょーです。 みなさんは、開発初期の段階でWeb API(以下API)の実装が追いつかずクライアント側が開発できないという経験をしたことはありますか? クライアント側はAPIがないと開発が滞ってしまうことがありますが、かといってAPIの開発も始まったばかりではすぐに必要なAPIを提供することができません。その問題を解決し、両者でスムーズに開発をすすめるために有効な方法の1つに、APIモックの作成があります。 弊社では、開発初期の段階でWeb APIのモックを作成し、スムーズに開発できるようにしています。 以前は、Apiaryをモック作成ツールとして利用していましたが、記法やエディターに使いづらい点があり最近Swaggerに移行しました。 本記事では、Swaggerを使ったAPIモックの作成方法と手順、また気をつけるべき点などを紹介します。 目次 Swaggerとは? Swagger記法の一例 Swaggerのエディター選択 SwaggerAPIモックの動かし方 結論 まとめ Swaggerとは? Swaggerとは、REST APIを定義するためのフレームワークです。また、それを記述するためのSwaggerEditorやドキュメントツールであるSwaggerUIなどの関連ツール群のことを指します。 Swaggerを使うメリット Swaggerを使うメリットとしては、下記のような項目があります。 YAMLの記法で書ける(※JSONでも書ける) YAMLで記述したAPI定義を元に、 多数の言語によるAPIモックアプリケーションを簡単に作成できる APIドキュメントの自動生成ができる 各エントリーポイントに対してパラメーターも指定できる AWSのAPI Gatewayに反映できる Swaggerで作るAPI定義はYAMLによる記述が可能なので、1回書いたモックを引用することができ、繰り返し同じダミーデータを書く必要がなくなります。 さらに、定義したYAMLファイルを元に下記のようなAPIドキュメントを自動的に生成してくれるSwaggerUIというツールがあります。 クライアントに渡すAPIドキュメントをいちいち手書きすることもなくなるのでとても便利です。 また、米国等ではSwaggerがREST API定義のデファクトスタンダードになっているようで、最近ではAWSのAPI GatewayでもSwaggerの記法をimportすることができるようになりました。今後様々なサービスでSwaggerが使えるようになる可能性があります。 Swaggerを使うデメリット デメリットとしては下記があると感じました。 周辺ツールが豊富すぎてベストな開発方法を見つけるのに時間がかかる REST APIの定義のために作られているため、モックで実現したい記法がドキュメントになく迷うことがある まずひとつとして周辺ツールが多いので開発を始める際、調査に時間がかかります。 これについては、エディターの説明やAPIモックサーバーの立ち上げ方でいくつかの開発手法を紹介します。 ふたつめの、迷いがちな記法については後日追って同テックブログにて公開します。 Swagger記法の一例 まずはイメージが湧きやすいように、シンプルなSwagger記法(YAML)の一例を載せます。 Swaggerの記法は ドキュメント にまとまっていますが、一読しただけでは少しわかりにくい部分があるので基本的な構成を紹介します。 info : # モックのメタデータを記述する schemes : # APIモックサーバーのプロトコルの選択 paths : # エンドポイントの記述 definitions : # モデル定義の記述 properties : # モデルの詳細定義 example : # 各値の例の記述 実際に上記の基本的な構成で記述したシンプルなSwagger YAMLがこちらです。 swagger : '2.0' # モックのメタデータを記述する info : description : | This is a sample server Petstore server. You can find out more about Swagger at [ http : //swagger.io ] (http://swagger.io) or on [ irc.freenode.net, #swagger](http://swagger.io/irc/). version : 1.0.0 title : Swagger Petstore license : name : Apache 2.0 url : http : //www.apache.org/licenses/LICENSE-2.0.html # モックサーバーのプロトコルの選択 schemes : - http # エンドポイントの記述 paths : /pet/{petId} : get : tags : - pet summary : Find pet by ID description : Returns a single pet operationId : getPetById produces : - application/json parameters : - name : petId in : path description : ID of pet to return required : true type : integer format : int64 responses : 200 : description : successful operation schema : $ref : '#/definitions/Pet' 400 : description : Invalid ID supplied 404 : description : Pet not found # モデル定義の記述 definitions : Category : type : object # モデルの詳細定義 properties : id : type : integer format : int64 name : type : string Tag : type : object properties : id : type : integer format : int64 name : type : string Pet : type : object required : - name - photoUrls properties : id : type : integer format : int64 category : $ref : '#/definitions/Category' name : type : string # 各値の例の記述 example : doggie photoUrls : type : array items : type : string tags : type : array items : $ref : '#/definitions/Tag' status : type : string description : pet status in the store enum : - available - pending - sold このような記法でAPIの内容を記述していきます。 こちらのYAMLから生成したAPIモックアプリケーションをローカルに立ち上げると下記のようになります。 ↓ http://localhost:8080/docs paths で指定したパスがAPIドキュメントになっているのがわかります。 下記が、各エントリーポイントのモックの一例です。 ↓ http://localhost:8080/pet/1 propertiesで指定したモデルの構成と、exampleで指定した実際の値がモックに反映されているのがわかります。 Swaggerのエディター選択 記法がなんとなくわかったところで、どのように開発を進めていくかを紹介します。 Swaggerのエディターには下記の3つが存在しています。 SwaggerOnlineエディター SwaggerHub のエディター Localエディター この3つのエディターのそれぞれのメリット、デメリットを説明します。 また、上記3つのエディターを使ず、手元の慣れたエディターで開発する手法についても紹介します。 SwaggerOnlineエディターを使う SwaggerOnlineエディターは、Swaggerが提供している無料のOnlineエディターです。機能が限定されていますが、シンプルな分扱いやすいのが特徴です。 アカウント作成の必要もなく、誰でもすぐ使うことができます。 http://editor.swagger.io/#/ 機能は本当にシンプルで、 手元にあるYAMLをimportする YAMLを編集してAPIモックアプリケーションを自動生成する 編集したYAMLをexportする くらいしかありません。 下記は、SwaggerOnlineエディターを使うことのメリット、デメリットの表です。 内容 メリット ・扱いが簡単 ・無料で使える デメリット ・機能が少ない 簡単なモックを作りたいだけのときにはこちらのエディターで十分だと思います。 SwaggerHubのエディターを使う SwaggerHub とは、OSSのSwaggerに幾つかの機能を追加して拡張したサービスで、様々な機能を提供してくれます。 OSSのSwaggerとSwaggerHubの機能の比較の表はこちらになります。 比較表 拡張機能の中でもメリットに感じた部分とデメリットに感じた部分を下記の表にしました。 内容 メリット ・チーム(Organization)を設定してエディターの共有ができる ・APIモックサーバーをホスティングしてくれる ・エディターの編集内容をGithubに自動連携できる デメリット ・Githubにpushしたコードをエディタに取り込むことはできない ・ほとんどの機能が有料 まず注目なのはGithubに自動連携できるということです。Githubのリポジトリと紐付け、エディターでアップデートした分を自動的にcommitしてくれます。これにより、リアルタイムで複数人の開発が可能になります。ただし、手元のエディター(vimなど)で編集し、GithubにpushしたものをSwaggerHubのエディターに取り込むことはできません。 また、デプロイ機能もついており、自前でホスティングしなくてもAPIモックサーバーを提供してくれます。 デメリットは、チームを設定する機能やGithubとの連携機能が有料なことです。登録初期には無料トライアル期間がありますが、数週間のみです。 下記は記事公開時点での無料/有料の機能表です。 (出典: https://app.swaggerhub.com ) 無料なのは、上の3つのみで後はほぼ有料となります。 SwaggerLocalエディターを使う 内容 メリット ・無料で使える ・pluginを書けばエディターを拡張できる デメリット ・環境を作る必要がある SwaggerLocalエディターはSwaggerOnlineエディターをlocalに立てたものです。 Localエディターの立て方は幾つか方法があるのでSwaggerToolsの公式ドキュメントとOSSとして公開されているコードを参照してください。 (参照: SWAGGER TOOLS DOCS , swagger-editor ) デフォルトの仕様ではSwaggerOnlineエディターと機能はほとんど変わりませんが、pluginを書くことで好きなように拡張できるのが特徴です。 特に拡張などをする気がない方はSwaggerOnlineエディターで十分だと感じましたが、好きなようにエディターを拡張したい方にはおすすめです。 手元の慣れたエディターを使う ここで誰しもが思うのが、手元の慣れたエディターで開発をすすめられないかということではないでしょうか。Swaggerのエディターを使わず、手元の慣れたエディターで開発する方法があります。 SwaggerのYAMLを手元のエディターで編集し、 SwaggerCodegen を使ってAPIモックアプリケーションを生成する手法です。SwaggerCodegenとは、各種言語のAPIモックアプリケーションを吐くコマンドツールです。 さらに、GithubでYAMLを管理すればチーム開発も可能です。編集するメンバーが少人数の場合はこちらの手法は有効です。 内容 メリット ・開発しやすい ・Githubで管理すればチームで共有可能 デメリット ・自前でホスティングする必要がある(localで立ち上げてテストや確認をする必要がある) ・Swaggerの文法エラーの詳細が出ない デメリットとして、他のSwagger専用エディターと違ってSwaggerの文法エラーの詳細を出してくれないという点があります。 その際には、SwaggerOnlineエディターやローカルエディターにYAMLをロードしてエラー箇所を確認するという手段を取る必要があります。そういった点から、こちらの手法はある程度Swaggerの開発に慣れて来た場合にオススメできる方法ですが、慣れたエディターを使うので開発効率が圧倒的に良かったです。 SwaggerAPIモックの動かし方 エディターを選出してSwaggerのYAMLを記述したら、実際にSwaggerのAPIモックを動かしてみましょう。 APIモックの動かし方は4つあります。 SwaggerCodegenを使ってAPIモックアプリケーションを自動生成する 各Swaggerエディター経由でAPIモックアプリケーションを自動生成する SwaggerHubを使う API Gatewayを使う SwaggerCodegenを使ってAPIモックアプリケーションを自動生成する 内容 メリット ・YAMLをロードするだけで簡単に動くサーバーが作れる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・自前でサーバーを用意する必要がある 手順 1. swagger.yamlを作成する swagger.yamlを好きなエディターで編集 2. swagger-codegenをlocalにダウンロード $ git clone https://Github.com/swagger-api/swagger-codegen $ cd swagger-codegen $ ./run-in-docker.sh mvn package (複数のやり方があるので自分の環境に合ったやり方を選択する 参照: SwaggerCodegen ) 3. コマンドでモックサーバーアプリケーションを生成する $ cd swagger-codegen $ java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate -i /path/to/swagger.yaml -l nodejs-server -o samples/server/test/nodejs (今回はnodejsのアプリケーションを作成) 4. 生成したサーバーを立ち上げる $ node ./samples/server/test/nodejs/index.js => Your server is listening on port 8080 (http://localhost:8080) Swagger-ui is available on http://localhost:8080/docs 5. /docs と各エントリーポイントを見てみる http://localhost:8080/docs にアクセスする ↓ http://localhost:8080/pet/1 一旦SwaggerCodegenをlocalにダウンロードした後は、手順 3. から下の手順のみになります。もしもGithubで共有している場合は、YAMLをGithubにPushして共有のサーバーでアプリケーションを立ち上げればチームでアプリケーションを共有できます。 各Swaggerエディター経由でAPIモックアプリケーションを自動生成する 内容 メリット ・YAMLをロードするだけで簡単に動くサーバーが作れる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・自前でサーバーを用意する必要がある 手順はシンプルで、各エディターのヘッダー部分にGenerateServerのボタンがあるのでそこから好きな言語を選択するだけでAPIモックアプリケーションの作成ができます。 上記で説明したエディター3種類のSwaggerEditorのどれもがこのGenerateServerの機能を提供しています。 SwaggerHubを使う 内容 メリット ・ホスティングしてくれる ・Swaggerの文法をそのまま適用することができ、exampleが書きやすい デメリット ・有料 こちらの画面がSwaggerHubのエディターです。 記述したエントリーポイントを開き、 Execute ボタンを押すとモックサーバーのURLを返してくれるのでそこにアクセスするだけです。 API Gatewayを使う 内容 メリット ・ホスティングしてくれる デメリット ・Swaggerでサポートされている文法とAPI Gatewayでサポートされている文法が微妙に違う(exampleが使えないなど) ・API定義をインポートした後のコンソール上での作業が多い 手順 SwaggerYamlをロードする 各エントリーポイントごとにMockサーバーを立てる 2. の手順が長いので、画面で説明します。 ①YAMLロード後の画面からエントリーポイントを選択後(今回の場合はpetsのGET)、総合リクエストを選択 ②Mockを選択 ③保存後、YAMLロード後の画面に戻って 総合レスポンス を選択後し、本文マッピングテンプレートでモックとして返したいモデルを選択 ④YAMLロード後の画面に戻り、テストを選択 こちらの画面でテストを選択後、モックのレスポンスが表示される ⑤YAMLロード後の画面に出ているURLにアクセスするとモックを見ることができる このように、モックにするとなると画面上での工程が多かったです。 エントリーポイントが複数ある場合においてはSwaggerCodegenやSwaggerHubのほうがオススメです。 また、API Gatewayではexampleが使えなかったり、definitionsのモデルを必ずキャメルケースで定義する必要があるなど、Swagger定義の仕様と異なる部分があったので手こずりました。 結論 エディターの選択とSwaggerAPIモックの動かし方を紹介しましたが、どのツールを使うかは好みやシーンによって様々です。 参考までに、弊社で採用した開発方法は下記のような感じです。 vimで編集したSwaggerYAMLをGithubで管理し、localでテストする際はSwaggerCodegenで生成したAPIモックアプリケーションをlocalで立ち上げてテストしています。また、テストできたらSwaggerYAMLを共用サーバーにデプロイし、SwaggerCodegenで生成したアプリケーションを立ち上げてチームで共通のモックアプリケーションを見られるようにしています。 上記を採用した理由としては、 手元の慣れたエディターで編集したい チームで1つのSwaggerYAMLを共有したい 無料 があります。 まとめ SwaggerはAPIのモックを作成してくれ、ドキュメントにもまとめてくれる機能があるのでとても便利です。ぜひSwaggerのAPIモックを試してみてください! 最後に VASILYでは、一緒にサービスを盛り上げてくれるエンジニアを探しています。 ぜひオフィスに遊びに来てください!
アバター
こんにちは。 インフラエンジニアの光野です。 先日の ブログ記事 でご紹介したとおり、弊社のクローラーはDockerコンテナ化されています。このコンテナはApache MesosとMarathonのクラスタ上で動いています。 先日の記事はクローラーシステム全体を取り扱いましたが、本記事ではMesos/Marathonを導入するにあたって必要だった設定について「〜したい」という形で紹介いたします。 Tips集として導入や検討の参考にしていただければ何よりです。 記事中の用語については先頭の 前提知識・用語まとめ にまとめています。また、Tipsは各見出しごとに独立させていますので、お好きな部分を参照ください。 シリーズ一覧 新クローラーシステムの全体観 Docker / Apache Mesos / Marathon による3倍速いIQONクローラーの構築 クラスタへのデプロイについて Production deployment of the Docker container with Marathon by Tatsuro Mitsuno クラスタ構築時のTips 本記事 Tips一覧 MesosのTips Tips 1. ホストポートをコンテナに割り当てたい(Mesos編) Tips 2. プライベートサブネット環境下でMesos UIを使いたい MarathonのTips Tips 3. ホストポートをコンテナに割り当てたい(Marathon編) Tips 4. タスクでUserDefinedNetworkを使いたい Tips 5. タスクの宣言をWeb APIで行いたい 前提知識・用語まとめ 本記事で使う用語や、コマンドを簡単にまとめます。 なお、Apache MesosとMarathon自体については先日の ブログ記事 で触れておりますので説明を省略します。 登場する用語 名称 概要 Mesosクラスタ Apache Mesosのマスタとスレーブから成る。zookeeperによって管理される Mesosマスタ Apache Mesosのマスタノード。スレーブに対してタスクを投入する。Web UIもここにある。 Mesosスレーブ Apache Mesosのスレーブノード。クラスタのリソースを担う。 Mesosエージェント Mesosスレーブの各ノードで動くデーモン。起動オプションによってMesosでできることが変わる。 タスク Apache Mesosで実行する処理。シェルコマンドからコンテナまでなんでもよい。本記事中では docker run されるもの。 ソフトウェアのバージョン Ubuntu 16.04.1 LTS Apache Mesos 1.1.0 Marathon 1.4.1 Mesos / Marathonの起動 Ubuntuの場合は、 Mesosphare社 がパッケージ化してくれており、 リポジトリを追加することで apt-get を使ってインストールが可能です。 # master sudo apt-get install mesos marathon sudo systemctl start mesos-master sudo systemctl start marathon # slave sudo apt-get install mesos sudo systemctl start mesos-slave 初期設定については以下の記事がとても参考になります。リポジトリについても記載されています。 なおOSバージョン毎に存在するパッケージが若干異なるためご注意下さい。 How To Configure a Production-Ready Mesosphere Cluster on Ubuntu 14.04 | DigitalOcean Mesosエージェントの設定方法 起動時にオプションとして与える mesos-agent --resources=ports:[80-80, 31000-32000] 設定ファイルに記述する オプション名 = ファイル名 ファイルの内容 = 引数 echo 'ports:[80-80, 31000-32000]' > /etc/mesos-slave/resources 本記事では2の方法を使っています。 ref. Apache Mesos - Configuration sudo systemctl restart mesos-slave なお、リスタートに失敗するようであればlatestディレクトリを削除して下さい。 sudo rm -rf /var/lib/mesos/meta/slaves/latest Mesosマスタはzookeeperを介して各スレーブを識別しており、その情報がlatestディレクトリに記録されています。 記録されている情報と新しい設定が食い違うとリスタートに失敗するため、latestを削除し新しいスレーブとして認識させます。 ref. Apache Mesos - Slave Recovery in Apache Mesos Marathonのタスク宣言 Marathonには大きく3つのタスク宣言方法がありますが、編集の手段が異なるだけで最終的なリクエストは同じJSONです。 Web UIで宣言する Web UIのJSONモードで宣言する Web APIで宣言する 本記事でも文中にMarathon用のJSONを記述します。 ただ、説明に必要な部分だけを抜粋しているため、コピー&ペーストでは動作しません。 MesosのTips Tips 1. ホストポートをコンテナに割り当てたい(Mesos編) 実行するタスクによってはホストのポートを専有したいことがあるかもしれません。 その場合、予めMesosエージェントに起動オプションを与えておく必要があります。 デフォルトで [31000-32000] が専有可能ですが、これに80番を追加する場合は次のように指定します。 echo ' ports:[80-80, 31000-32000] ' > /etc/mesos-slave/resources sudo systemctl restart mesos-slave Tips 2. プライベートサブネット環境下でMesos UIを使いたい MesosはWeb UIを持っており、ここからクラスタやタスクの状況、またタスクごとのサンドボックスを確認することができます。 サンドボックス内には、fetch済みのファイルやログファイルがあるためデバッグ時に大変便利です。 この情報は、Web UIがMesosスレーブに対して直接リクエストを行い収集しています。 一方、AWSのベストプラクティスに従うとMesosクラスタはプライベートサブネットに構築されることが多いと思います。 実際に、弊社のMesosクラスタは次の構成になっています。 MesosのWeb UIでサンドボックスの中を確認するためには、手元からプライベートサブネットにあるMesosスレーブへアクセスする必要があります。 間にELBとnginxによるプロキシを挟みこれを解決します。 Mesos Config Web UIがスレーブにアクセスする場合、その問い合わせはMesosエージェントに起動オプションとして与えられたホストネームに対して行われます。 まず、nginxで扱いやすいユニークな名前を設定してください。 echo " $( hostname ) .mesos-slave.xxxxx.yyyyy " > /etc/mesos-slave/hostname sudo systemctl restart mesos-slave xxxxx / yyyyyは、適宜ご自身で所有されているドメインへ読み替えてください。 DNS Record ELBに対するAliasレコードとして *.mesos-slave.xxxxx.yyyyy を設定してください。 nginx nginxを使って、特定のルールに基づくホストネームから名前解決を行い、 プライベートサブネットに存在する各スレーブへプロキシします。 なお、コメントにもありますが、5051ポートはMesosエージェントが利用するためnginxは別ポートでListenしています。 server { set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For; # [NOTE] 5051はMesosエージェントがbindする # ELBでポートを変えて送信。 # Web UI -> 5051 ELB -> 15051 nginx -> 5051 Mesosスレーブ listen 15051; server_name .mesos-slave.xxxxx.yyyyy; location / { # [NOTE] Route53 resolver 10.0.0.2; # [NOTE] 定期的に名前解決を行えるようにsetする # 直接書くとnginx restartでしか名前解決が行われない if ($host ~* (.*)\.mesos-slave\.xxxxx\.yyyyy) { set $mesos_slave_server "$1.YOUR_AWS_REGION.compute.internal" ; } proxy_pass http://${mesos_slave_server}:5051; } } ここではRoute53をVPC内のprivate DNSとして使っています。 MarathonのTips Tips 3. ホストポートをコンテナに割り当てたい(Marathon編) Marathonにおいて特定のホストポートを専有するタスクはスケジューリングに制約を与えることから非推奨になっています。 とはいえ実行するタスクによってはホストのポートを専有したいことがあるかもしれません。 この場合、Marathonでタスクを宣言する際にオプションを与える必要があります。 ポートを割り当てる(入門編) まずは公式ドキュメントに従って単純に設定します。 { " container ": { " type ": " DOCKER ", " docker ": { " network ": " BRIDGE ", " requirePorts ": true , " portMappings ": [ { " containerPort ": 3000 , " hostPort ": 80 , " protocol ": " tcp " } ] } } } requirePortsをtrueに設定 portMappingsでhostPortを0ではない値に設定 hostPortで指定するポート番号は、予め Mesosエージェントに起動オプション で許可されている必要があります。  これらは、公式ドキュメントの トラブルシューティング にわかりやすくまとめられています。 ポートを割り当てる(実践編) 入門編の内容で、ポートを割り当てる事自体は完了です。実践編ではデプロイ時の問題を解決します。 { " container ": { " type ": " DOCKER ", " docker ": { " network ": " BRIDGE ", " requirePorts ": true , " portMappings ": [ { " containerPort ": 3000 , " hostPort ": 80 , " protocol ": " tcp " } ] } } , " constraints ": [ [ " hostname ", " UNIQUE " ] , // あるタスクがMesosスレーブあたり高々1コンテナになるようにする ] , " upgradeStrategy ": { " minimumHealthCapacity ": 0 , // デプロイ時、旧コンテナ数が0になることを許容する " maximumOverCapacity ": 0 // デプロイ時、旧タスクをkillしてから新タスクをrunする } } requirePorts と hostPort に加えて、 constraints と upgradeStrategy を設定しています。 Marathonはタスクを更新する際、 upgradeStrategy とに基づいてタスクを ローリングリスタート してくれます。 新しいタスクが何らかのバグで起動しない場合も、古いタスクがそのまま動き続けるため安全です。 しかし、既存のタスクがホストポートを専有するタスクの場合、新しいタスクはポートをバインドできずデプロイが必ず失敗するという状況に陥ります。 その為、 constraints を使ってタスクのスケジューリングに制約を与えた上で、 upgradeStrategy を変更して旧タスクが無い状況を作り出すことで問題を回避します。 幸いにも弊社で運用されているポートを専有するタスクは、最悪瞬断しても良いという類のものでした。 もし、瞬断が許されない条件で動かす場合は、別の工夫が必要になります。 Tips 4. タスクでUserDefinedNetworkを使いたい DockerのUDNを使いたい場合は、3箇所の宣言が必要です。 { " container ": { " type ": " DOCKER ", " docker ": { " network ": " USER " } } , " ipAddress ": { " networkName ": " mesos_slave_host_nw " } , " ports ": [] } network にUSERを指定 ipAddress にUDN名を指定 docker network create したときの名前 ports に空配列を指定 空配列を明示的に指定しないと、エラーになります。 portsはオプショナルな項目のため、ついつい忘れがちです。ご注意ください。 Tips 5. タスクの宣言をWeb APIで行いたい Marathonは整理された Web API をもっています。 Web APIには コンソール も用意され、各パラメータの詳細を確認することが可能です。 Web APIはRESTfulに設計されており、状況に応じてPOST/PUT/PATCH/DELETEを使い分けます。 POST: 新タスクの宣言 PUT: 既存タスクの更新 / もし既存タスクがなければ作成される PATCH: 既存タスクの更新(1.4.1時点で /v2/apps 以下のエントリポイントのみ) DELETE: タスクの削除 操作したいリソースに対するエントリポイントさえ分かれば自然に利用できるかと思いますが、 その中で /v2/apps に対するPUTについては注意が必要です。 curl -XPUT -H " Accept: application/json " -H " Content-type: application/json " < Mesosマスター > /v2/apps/ -d@app .json PUTはとても便利でPOST/PATCHの両方を兼ねてくれるのですが、 Marathon 1.4系 からPATCHのように振る舞うPUTについてDeprecatedになりました。 後方互換性を守るため、1.4.1時点ではPUTとPATCHに挙動の差はありません。ただし、次のバージョン(おそらく1.5.0)で変更されるという宣言がされています。 For backward compatibility, we will not change this behaviour, but let users opt in for a proper PUT. The next version of Marathon will use PATCH and PUT as two separate actions. 将来のバージョンアップを考えると、PATCHを積極的に利用するのが望ましいです。 余談ですが、1.4.0には「PATCHのように振る舞うPUTがすべてエラーになる」という不具合がありUIも一部動作しません。そのためアップデートの際には1.4.1以降を選択下さい。 おわりに Apache MesosとMarathonを本番運用するにあたって必要になるであろう内容をTipsの形でご紹介いたしました。 Apache MesosとMarathonは実際に運用している情報が少なく、発生する問題に対しては自力で解決する必要があります。 とはいえ、両者とも機能そのものは豊富ですし、なにより公式の情報がとても丁寧に整備されています。 そのため、ドキュメントさえ読み込めば大抵のことはフレームワーク上で解決できるというのが、実際に運用してみての感想です。 今後もまた問題が発生するとは思いますが、都度ドキュメントとにらめっこして解決して解決していこうと考えています。 最後に VASILYにはこんなトライ&エラーを繰り返しながら成長できる環境があります。皆様の応募をお待ちしております。
アバター
VASILYのiOSエンジニアにこらすです。最近、 Swift Evolution に私の2つ目の提案がマージされました。 今回は、Swiftで型にExtensionを作る特殊な方法について説明します。 今回紹介する方法を使ってExtensionを作ると、名前空間が切り分けられ、コードの読み書きがしやすくなります。 ブログを書くに当たって、この Extension 実装方法を研究しましたが、この手法の正確な名前がわからなかったため、この記事では「Targeted Extensions」と呼ぶことにします。 Extensionについて 通常、 Extensionを書くとき、 String なら下記のようになります。 extension String { var count : Int { return characters.count } } "hello" .count // 5 Extensionを作ることで .characters.count を .count だけで書くことができます。しかし、便利ではありますが、コードを読む時 .count プロパティは String に元からあるものなのか、Extensionで定義されたオリジナルのプロパティなのかがわかりません。 さらに、将来のSwiftのアップデートで同じ名前のプロパティが追加されてExtensionのプロパティと衝突してしまう可能性もあります。 これらの問題を回避するためには衝突を避けるような別のExtension実装方法が必要になります。 衝突を避けるために "some string".my_length のように『Extensionのメソッドにプレフィックスを付ける』という命名規則を採用することもできますが、言語の仕組みでこの命名規則を強制するような仕組みはありません。 "some string".ex_length ではなく "some string".ex.length と書けるなら、よりSwiftyな感じがしますが、どのようにすればよいのでしょうか? それが今回のこの記事のテーマです! 最近では、 RxSwift , Kingfisher , ReactiveSwift などのOSSライブラリがこのExtensionの書き方を採用しています。 例を見よう: RxSwiftの場合 リアクティブプログラミングライブラリの RxSwift では rx というキーワードを使って、オリジナルのクラスに属するものと、 RxSwiftに属するものを明確に区別できるようになります。 また、Xcodeでのコード補完もキレイ動作します。 実際のコード例です。 myButton.rx .controlEvent(.touchUpInside) .subscribe(onNext : { _ in // ボタンをタップした時、ログに流れる print( "Hello There!" ) }) String の場合は? 先ほどの String のExtensionを下記のように書けると良いです。 "hello" .ex.count // 5 ex があることによって、名前空間が分けられます。そのため、他のサードパーティーライブラリが String に count というプロパティを作ったとしてもプロパティ名が衝突することがありません。 さて、Targeted Extensionsはどのように動作しているのでしょうか? ここから、このExtensionの動作について説明します。 5つのステップに分かれているので1つずつ説明しますが、それほど大きくないので、一旦全てのコードを貼っておきます。 下記のコードをXcodeのPlaygroundにコピー&ペーストすればそのまま動作します。 public protocol ExampleCompatible { associatedtype CompatibleType var ex : CompatibleType { get } } public final class Example <Base> { let base : Base public init (_ base : Base ) { self .base = base } } public extension ExampleCompatible { public var ex : Example <Self> { return Example( self ) } } extension String : ExampleCompatible { } extension Example where Base == String { public var count : Int { return base.characters.count } } "hello" .ex.count // 5 ステップ1: ex の定義 "hello".ex.count を見ると、 String が ex というプロパティを持っているはず。 どこで定義されているか確認しましょう。 public protocol ExampleCompatible { associatedtype CompatibleType var ex : CompatibleType { get } } 上記のプロトコルを採用したら ex というプロパティを持ってないといけません。 次は ExampleCompatible を採用しましょう! ステップ2: ExampleCompatible extension String : ExampleCompatible { } String が ExampleCompatible を採用していることがわかります。 しかし、 ex の実装がありません。 ステップ3: ex の実装 public extension ExampleCompatible { public var ex : Example <Self> { return Example( self ) } } まだ説明していませんが、 Example<Self> 型の変数を返しています。 Self は ExampleCompatible を採用している String になり、 self は "hello" になるはずです。 色々な型で実現するために、ジェネリクスを使ってプロトコルを直接拡張します。そうすることで String だけではなく NSString や Int でも同じ名前空間 を ( ex ) 使って様々な型を拡張することができます。 実際に様々な型に対応した場合、下記のようなコードになります。 "Yeah" .ex.count 1024 .ex.foo [ "a" , "bc" , "def" ].ex.hoge このコードを見ると、 foo や hoge が count と同じ Example の名前空間に宣言されたものと分かります。 ステップ4: Example public final class Example <Base> { let base : Base public init (_ base : Base ) { self .base = base } } Example<Base> は Base 型のプロパティを1つだけ持ったクラスです。 ここで Base は、ステップ3で説明したように String になります。 ステップ5: count の実装 extension Example where Base == String { public var count : Int { return base.characters.count } } Example<Base> の Base が String の時だけ、動作するExtensionが宣言されています。 base は String なので、実際には "hello" が入っているはずです。 ここでやっと "hello" の文字数を返すことができます。 String 以外の実例 Targeted Extensions でどんな型でも拡張することができます。 例えば Int に偶数か奇数というプロパティを追加するには、 String と同じように2つのステップで書けます。 まず Int を ExampleCompatible に拡張します。 extension Int : ExampleCompatible { } こうすると Int は ex というプロパティを持つので、 String と同じようにExtensionを書けます。 extension Example where Base == Int { var isEven : Bool { return base % 2 == 0 } } 1 .ex.isEven // false 2 .ex.isEven // true パフォーマンスについて ex がアクセスされるたびに、新しい Example インスタンスが生成されるため、若干のパフォーマンスの差があります。 // 通常のExtension extension String { var length : Int { return characters.count } } // 名前空間を使ったExtension extension Example where Base == String { var length : Int { return base.characters.count } } 第5世代 iPod touch で検証した結果 (5回テストした平均値) 実行回数 通常のExtension (sec.) Targeted Extensions (sec.) 100 0.02140 0.02239 1000 0.02475 0.02875 1000000 2.32037 4.12863 このパフォーマンステストの結果からわかるのは、Exampleインスタンスを 8848回生成して、やっと1フレーム(60FPSのとき16ms)の遅延が発生することになり、このパフォーマンスの遅延は無視できると言えます。 0.016 / ((4.12863 - 2.32037) / 1000000) ≒ 8848 回 こんな場面で導入しました 最近、私のライブラリでTargeted Extensionsを導入しました。良い実例になると思いますので、参考にしてみてください。 Nirma/Attributed NSAttributedString を型安全に書けるようにするライブラリです。(是非スターを付けてください) let attributedText : NSAttributedString = "Steve" .at.attributed { return $0 .font(UIFont(name : "Chalkduster" , size : 24.0 ) ! ) .foreground(color : .red) .underlineStyle(.styleSingle) } まとめ この Extension の書き方によって、名前空間ができるため、メソッド名の衝突を避けることができます。 また、コードを読むときも、名前空間があることで、拡張されたメソッドなのか、元からあるメソッドなのかがわかりやすくなります。 Targeted Extensionsは少し複雑ですが、上記のようなメリットがあります。 VASILYでもSwiftの言語仕様に追加されない限りはTargeted Extensionsを採用していく予定です。 P.S なお、今回紹介した Targeted Extensions について、正しい名前をご存知の方は教えていただけると嬉しいです。 VASILYではモダンなSwiftコードを書きたいエンジニアを募集しています。 少しでも興味がある方は以下のリンク先をご覧ください。 https://www.wantedly.com/projects/88978 www.wantedly.com にこらす
アバター
こんにちは、Androidエンジニアの堀江です。最近はiOSのプロジェクトに参加してSwiftを書いています。新しいことを始めるのは楽しいですね。 ところで今ご覧になられている弊社の技術ブログ「 VASILY DEVELOPERS BLOG 」は、VASILYのエンジニアが交代で更新しています。記事に何を書くかは各エンジニアの裁量に任されていますが、公開前に社内でレビューをするようにしています。 レビューをする際には、以下のような点に注意しています。 誤字脱字・文法上の間違いが無いか 間違った情報が無いか 文章中にわかりにくい表現や解説が無いか このうち、誤字脱字・文法上の間違いは、文章校正ツールを使うことで機械的にチェックすることが可能です。それによって、文章そのもののより本質的なレビューに時間を割くことができます。記事はレビュー前に文章校正済みであるのが理想ですが、実際には忘れる事も多いです。そのため、ある程度自動的に文章校正を実行してくれると捗ります。 本記事では、botを利用することで文章校正の実行を自動化する試みについてご紹介します。 記事公開までの流れについて 記事の公開までの流れは、概ね以下の通りになっています。 記事を執筆 記事のレビュー 指摘点の修正 公開 VASILYでは社内の情報共有に Qiita:Team を使用しており、ブログ記事についてもある程度書いた段階でQiita:Teamに投稿してしまいます。普段どのようにQiita:Teamを使っているかは、以下の記事にまとめて頂いています。 https://codeiq.jp/magazine/2017/04/50255/ codeiq.jp そして、記事の執筆が完了するとSlackのエンジニアチャンネルにレビュー依頼を投げ、指摘を受けた点について修正後、公開します。また、何時誰が記事を書くかはスケジュールを予め決めています。(歴史的経緯により社内ではDEVELOPERS BLOGの事をテックブログと呼んでいます) どう自動化するか? 記事がQiita:Teamに投稿・更新された際に文章校正を自動で実行します。Qiita:Teamで発生する様々なイベントは Webhook で受取ることができるので、記事が投稿・更新されたタイミングで任意の処理を実行可能です。Webhookの受け取りには、Heroku上にホストした Hubot を利用します。そして、文章校正には textlint を使用します。 全体の流れ 全体の流れを図にすると以下のようになります。 ① 記事の投稿・更新 ② HubotがWebhookを受け取り記事本文を取得 ③ 記事本文に対してtextlintで文章校正を実行 ④ 文章校正の結果を取得 ⑤ 対象の記事に文章校正の結果をコメント ⑥ 文章校正の結果を確認 ②での記事本文の取得、⑤でのコメント投稿には、 Qiita API v2 (以下Qiita API)を使用します。 Webhookの概要と設定方法 Qiita:DeveloperのWebhooks にWebhookの仕様がまとまっています。ドキュメントより、Qiita:Teamから受け取れるイベントの一覧は以下のようになっています。 イベント名 概要 ItemCreated 記事が作成されたときに送信 ItemDestroyed 記事が削除されたときに送信 ItemUpdated 記事が更新されたときに送信 CommentCreated コメントが作成されたときに送信 CommentDestroyed コメントが削除されたときに送信 CommentUpdated コメントが更新されたときに送信 MemberAdded チームメンバーが追加されたときに送信 MemberRemoved チームメンバーが離脱したときに送信 PingRequested Webhookの設定画面からテストを行ったときに送信 ProjectCreated プロジェクトが作成されたときに送信 ProjectDestroyed プロジェクトが削除されたときに送信 ProjectUpdated プロジェクトが更新されたときに送信 リクエスト イベントが発生すると指定したURLにPOSTリクエストが送信されます。リクエストボディにJSON形式でエンコードされた文字列が含まれ、これを Payload と呼びます。 また、共通リクエストヘッダとして X-Qiita-Event-Model に Payload の model プロパティと同様の値が、 X-Qiita-Token にWebhookに割り当てられたトークンが含まれます。 Payload Payload には、必ず action プロパティと model プロパティが含まれ、 action プロパティは「どのイベントが発生したか」を表す文字列で、 model プロパティは「何に対してイベントが発生したか」を表す文字列です。この他に各イベントに関連するデータが含まれます。 例えば、記事作成を表すItemCreatedイベントの場合、Payloadは以下の様になります。その他のイベントについては 公式ドキュメント を参照してください。 プロパティ名 型 説明 action String "created" model String "item" item Item 作成された記事 user User 作成したユーザ Item 、 User 型がどのようなプロパティを含むかは、 公式ドキュメントのTypes から確認できます。 設定方法 Webhookのリクエスト先や、通知したいイベントの設定は、設定メニューから設定をします。通知したいイベントにチェックを付け、URLにはイベントを送信したいURLを指定します。Webhookの設定はチームの管理者のみ設定メニューに表示されます。 textlintの設定と実行 textlint はテキスト向けのLintツールで、予め定義したルールに沿って文章校正を行ってくれます。textlintの利用方法としてCLIから利用することが多いですが、今回は、Hubotのコードからモジュールとして実行します。 ここでは、textlintの設定とモジュールとしての実行方法について紹介します。ローカルで動作を確認できるようサンプルプロジェクトを用意しました。 github.com サンプルプロジェクトのセットアップ サンプルプロジェクトをcloneし npm install を実行します。Node.jsのバージョンは v6.10.2 で動作確認をしています。Node.jsをインストール済みで無い場合、 nvm や nodebrew 等を使用してインストールするとNode.jsのバージョンを簡単に切り替えることができます。 git clone git@github.com:horie1024/textlint-sample.git cd textlint-sample npm install .textlintrcの設定 サンプルでは、textlintのルールとして textlint-ja/textlint-rule-preset-ja-technical-writing を使用しています。 .textlintrc は以下のようになります。 { " rules ": { " preset-ja-technical-writing ": true } } モジュールとしてtextlintを実行 モジュールとしてtextlintを実行するコードは以下のようになります。 TextLintEngine をインスタンス化する際に configFile に .textlintrc のパスを指定します。そして、 TextLintEngineCore#executeOnText を使用すると引数に取ったテキストに対してtextlintを実行することができます。引数にファイルを指定したい場合 TextLintEngineCore#executeOnFiles を使用します。 const TextLintEngine = require( "textlint" ).TextLintEngine; const engine = new TextLintEngine( { configFile: "config/.textlintrc" } ); engine.executeOnText( "# test!!" ).then(results => { console.log(results [ 0 ] .messages); if (engine.isErrorResults(results)) { const output = engine.formatResults(results); console.log(output); } } ); 実行結果は以下のように出力されます。 実装 ここまでで紹介した、Qiita:TeamのWebhookとtextlintのモジュールとしての実行を組み合わせて以下の流れを実現します。 サンプルプロジェクト 作成済みのHubotプロジェクトをサンプルプロジェクトとして用意しました。こちらをclone後rootディレクトリで npm install を実行することでローカルでHubotを動かせるようになります。 github.com Node.jsのバージョンは v6.10.2 で動作確認をしています。 この記事では、Heroku上でHubotをホストするための設定方法は紹介しませんが、Web上に良い記事が多くあります。参考にしてみてください。 文章校正を実行する条件 Webhookでは、全ての記事の投稿・更新を受け取れるので、記事に対して文章校正を実行するかを判断する必要があります。Qiita APIを使用すると、タグだけでなく投稿者やコメント等を取得できますので、これらの情報を判別に用いる事も可能です。 今回は、以下の条件に当てはまる場合に文章校正を実行します。 記事にタグ techblog が付いている WebhookのPayload中の model が "item" 、 action が "created" もしくは "updated" である Webhookでの各種イベントの受け取り 今回はHeroku上にホストした Hubot でWebhookを受け取ります。Qiita:TeamからのWebhookはPOSTリクエストですので、Hubotの HTTP Listener を利用してイベントを受け取ります。 実際のコードは以下のようになります。このコードをHubotプロジェクトのscriptsディレクトリ以下に配置します。Webhookを受け取るためのパスは /qiita/webhooks とします。Webhookの設定で入力するURLは、 https://Herokuアプリ名.herokuapp.com/qiita/webhooks となるはずです。 module.exports = robot => { robot.router.post( '/qiita/webhooks' , (req, res) => { console.log(req.body); res.end(); } ); } URLの設定後、例として以下のような投稿をQiita:Teamで作成してみます。 その結果、Qiita:TeamからWebhookのPOSTリクエストが送信され、以下のようなリクエストボディを受け取ることができます。 { action : 'created' , item : { body : '<p>test</p>\n' , coediting : false , comment_count: 0 , created_at_as_seconds: 1492484003 , created_at_in_words: '1分未満' , created_at: '2017-04-18 11:53:23 +0900' , id : 12345 , lgtm_count: 0 , raw_body: 'test\n' , stock_count: 0 , stock_users: [] , tags : [] , title : 'test' , updated_at: '2017-04-18 11:53:23 +0900' , updated_at_in_words: '1分未満' , url : 'https://vasily.qiita.com/Horie1024/items/1234567890abcdefghijk' , user : { id : 12345 , profile_image_url: 'https://qiita-image-store.s3.amazonaws.com/profile-images/xxxxxx' , url_name: 'Horie1024' } , uuid : '1234567890abcdefghijk' } , model : 'item' , user : { id : 12345 , profile_image_url: 'https://qiita-image-store.s3.amazonaws.com/profile-images/xxxxxx' , url_name: 'Horie1024' } } リクエストボディの action プロパティが created 、 model が item であることから記事の新規作成であることがわかります。 また、 item プロパティはItem型で表され、ドキュメントの Types#Item でItem型の各プロパティがどのような意味を持つのか確認できます。例えば、 raw_body プロパティは記事本文、 body プロパティは記事本文のHTML表現を表します。 .textlintrcの設定 検証ルールは、 書籍執筆用のルール をベースにVASILY DEVELOPERS BLOGの体裁に合わせてカスタマイズしています。簡単な表記ゆれの対策には、 textlint-rule-prh を利用しています。 { " rules ": { " prh ": { rulePaths : [ " prh.yml " ] } , " max-ten ": { max : 3 } , " spellcheck-tech-word ": true , " no-mix-dearu-desumasu ": true , " no-exclamation-question-mark ": false , " preset-ja-technical-writing ": { " no-exclamation-question-mark ": { " allowHalfWidthExclamation ": false , " allowHalfWidthQuestion ": false , " allowFullWidthExclamation ": true , " allowFullWidthQuestion ": true } } , " preset-jtf-style ": true } } 以下はprhの定義ファイルです。 version : 1 rules : - expected : IQON pattern : - iqon - iQON - expected : VASILY pattern : - vasily - Vasily Webhookからの記事本文の取得とtextlintの実行 Webhookからの記事の投稿・更新を受け、textlintでの文章校正を実行します。 TextLintEngineCore#executeOnText を使用すると引数に取ったテキストに対してtextlintを実行することができます。第二引数にはMarkdown形式のファイルである事を示すために .md を指定します。そして、校正結果について TextLintEngineCore#isErrorResults で確認し、エラーが有ればエラー内容をQiitaのコメントで確認しやすいようフォーマットします。 robot.router.post( "/qiita/webhooks" , (req, res) => { let item = req.body.item; // Webhookで受け取った記事本文をexecuteOnTextに渡す // 第二引数には".md"を指定 engine.executeOnText(item.raw_body, ".md" ).then(results => { if (engine.isErrorResults(results)) { // エラー有り // 結果を確認しやすいようフォーマット } else { // エラー無し } } ); } ); 文章校正の結果をフィードバック 文章校正の結果は、記事へのコメントとしてフィードバックします。コメントの投稿・編集には Qiita API を通じて行うため、 簡単なAPIクライアント を作成しています。 以下のコードでは、校正結果をコメントとして投稿しています。 const Qiita = require( '../libs/Qiita' ); const qiita = new Qiita( { team: "vasily" , token: process.env.YOUR_QIITA_TOKEN } ); robot.router.post( "/qiita/webhooks" , (req, res) => { let item = req.body.item; // Webhookで受け取った記事本文をexecuteOnTextに渡す // 第二引数には".md"を指定 engine.executeOnText(item.raw_body, ".md" ).then(results => { if (engine.isErrorResults(results)) { // エラー有り let output = ... // 結果を確認しやすいようフォーマット qiita.Comments.post(item.uuid, output); } else { // エラー無し qiita.Comments.post(item.uuid, "エラーはありません" ); } } ); } ); また、既に文章校正の結果が投稿されている場合、コメントを上書きするようにしています。 ここまでのコードの詳細はこちらを御覧ください。 https://github.com/horie1024/hubot-textlint-sample/blob/master/scripts/hubot-textlint.js 実行結果 Qiita:Teamに techblog タグを付けて投稿・編集すると以下のように校正結果がコメントとして投稿されます。自分で自分にフィードバックしている形になっていますが、本来ならbot用にサービスアカウントを用意したいところです。Qiita:Teamでサービスアカウントが利用できないため、暫定で自分で発行したアクセストークンを使用しています。 まとめ Qiita:Teamへの投稿をトリガーに文章校正を実行することができました。textlintは非常に素晴らしいツールで、既存の他のツールと連携させることも簡単に行うことができます。校正ルールはまだ見直しの余地がありますが、今回の事を切っ掛けにみんながより文章を書きやすい環境を作っていけたらと思います。 最後に VASILYでは、現在新規サービスの開発を行っています。少しでもご興味のある方のご応募をお待ちしています。
アバター
こんにちは、フロントエンジニアの茨木です。 本記事ではRailsアプリでクロールディレクティブを安全・効率的に設定する仕組みをご紹介したいと思います。 Web上にあるページは、クローラーと呼ばれるロボットに巡回されて検索エンジンにインデックス登録されます。大規模なサイトにおいてはページを効率よくインデックス登録させる必要があります。その際にクロールディレクティブと呼ばれる様々な設定が必要ですが、管理が複雑になってきます。この問題に対して、VASILYでの解決方法をご紹介します。同じような境遇の方々の参考になれば幸いです。 クロールディレクティブとは クロールディレクティブは、クローラーにサイト巡回の仕方を伝えるための設定です。これにより、クローラーに対してページをインデックス登録させない、ページ内のリンクを辿らせないといった設定を行うことができます。クロールディレクティブによりどのような設定ができるかはGoogle 公式ページ にも記載があります。 ここでは、弊社で重要視している3つのクロールディレクティブをご紹介します。 noindex noindexはページをインデックス登録させないことを示すディレクティブです。これを重複コンテンツや低品質コンテンツのページに指定することで、サイトの評価低下やそれによる クローラーリソース の割当減少を防止できます。noindexをhtml内で設定する場合にはheadタグ内に以下のタグを挿入します。 < meta name = "robots" content = "noindex" > nofollow nofollowは、クローラーにページ内のリンクを辿らせないことを示すディレクティブです。noindexを指定したページへのリンクにnofollowを指定することで、クローラーリソースの無駄遣いを防止できます。nofollowはheadタグ内のmetaタグか任意のaタグで指定が可能です。metaタグで指定した場合にはページ内の全リンクに、aタグで指定した場合には自身のリンクにnofollowが設定されます。 例1) metaタグでのnofollow設定 < meta name = "robots" content = "noindex" > 例2) aタグでのnofollow設定 < a href = "hogehoge.html" rel = "nofollow" > リンク </ a > canonical canonicalは正規URLを示すディレクティブです。同じコンテンツに複数のURLでアクセスできる場合、非正規URLのページにcanonicalを設定することでクローラーに正規のURLを通知することができます。canonicalはheadタグ内のlinkタグで指定が可能で、href属性で正規URLを設定します。 例) linkタグによるcanonical設定 < link rel = "canonical" href = "https://www.iqon.jp/" > IQONにおけるクロールディレクティブと課題 クロールディレクティブの仕様 IQONは、アイテム・コーディネート・相談・記事といった多くのコンテンツを持っています。そして、それらの各ページにクロールディレクティブを設定しています。コンテンツの中には検索エンジンからのランディングに適さないページもあり、そのようなページにはnoindexを指定しています。 以下の表はURLとそれに対応するクロールディレクティブの例です。 URL index/noindex canonical https://www.iqon.jp/ (トップページ) index - https://www.iqon.jp/ask/ (相談ページ) noindex - https://www.iqon.jp/ask/solved/ (解決済み相談ページ) index - https://item.iqon.jp/20219512/ (アイテム詳細ページ) index https://item.iqon.jp/20219511/ https://item.iqon.jp/brand/a.v.v/18/ジャケット/?price_max=30000 (アイテム検索ページ) noindex - アイテム検索ページにはカテゴリ、価格、袖丈といった様々な絞込条件があります。絞込条件によっては検索エンジンからのランディングに適さない場合もあるので、絞込条件に応じてnoindexを設定しています。「a.v.v、ジャケット、長袖、5000円以下、セール」で検索した場合は、以下のようなURLになります。 https://item.iqon.jp/brand/a.v.v/18/ジャケット/?price_max=5000&sleeve_length=長袖 各絞込条件に応じてnoindexを制御しているので、以下の例のようにロジックが複雑になっています。 uri = URI .parse(request.original_url) path = uri.path query_params = URI .decode_www_form(uri.query).to_h if path !~ /^\/ brand \// if query_params.price_max.to_i > 50000 || query_params.sleeve_length.present? @noindex = true end elsif query_params.price_max.to_i > 100000 @noindex = true end if @items .length < 10 @noindex = true end 課題 これまでに述べたように、IQONでは複雑なクロールディレクティブを設定しています。そのため、以下の課題がありました。 noindexページへのリンクにnofollowを効率よく設定できない クロールディレクティブが複雑なために設定ミスが発生する nofollowは先に述べた通り、各noindexページへのリンクに対して設定するのが望ましいです。しかし、そのためにはそれぞれのリンクにおいてnoindex設定条件を考慮しなければなりません。これを愚直にやるのは非効率なだけでなく、メンテナンス性の観点でも良くありません。 また、複雑なクロールディレクティブは設定ミスも引き起こすことがあります。特にnoindexの設定ミスは、インデックス数減少に伴うPV数減少など、致命的な問題を引き起こしかねません。 そこで、上記の課題を解決するための仕組みをご紹介します。 noindex・nofollow設定を効率化する仕組み 設定ロジックの共通化 noindex・nofollow設定の効率化のために、まずnoindex設定ロジックの共通化が必要です。IQONではnoindex設定ロジックを管理するNoindexManagerというクラスを定義し、それを任意の場所で利用できるようにしました。 lib/noindex_manager.rb class NoindexManager def initialize (url, context) @url = url @context = context end # 実際に判定を行うインスタンスメソッド。noindexの場合にtrueを返す。 def noindex? # 末尾にスラッシュがあるとうまく認識できないので予め正規表現で削る path = Rails .application.routes.recognize_path( @url .sub( /\/$|\/\?.*/ , '' )) # アクションに対応する判定ロジックを呼び出す send( " check_ #{ path[ :controller ] } _ #{ path[ :action ] }" .to_sym) end # アイテム検索ページのnoindex判定ロジック def check_item_index uri = URI .parse( @url ) path = uri.path query_params = URI ::decode_www_form(uri.query).to_h if path !~ /^\/ brand \// if query_params.price_max.to_i > 50000 || query_params.sleeve_length.present? return true end elsif query_params.price_max.to_i > 100000 return true end if @context [ :item_count ] < 10 return true end return false end end NoindexManagerはURLとコンテキストを用いてnoindexの判定を行います。コンテキストには「URLに含まれないがnoindex判定に用いられる値」を渡します。これにより、URLに含まれないアイテム件数などを判定条件に含めることができます。 NoindexManagerはURLとコンテキストによってインスタンスが生成され、インスタンスメソッドNoindexManager#noindex?によって実際の判定が行われます。 NoindexManager .new( ' https://item.iqon.jp/ ' , { item_count : 150 }).noindex? ここでは説明のため省略していますが、実際には簡単な判定ロジックの場合にYAMLファイルで設定できるような仕組みがあります。 nofollowを考慮したリンク生成ヘルパー 前述のNoindexManagerを更に便利に使うために、nofollowを考慮できるリンク生成ヘルパーを定義しています。Rails標準のヘルパーであるlink_toと同様のインターフェースで使用できるようにしています。NoindexManagerで用いられるコンテキストは、オプションのcontextキーで設定できるようになっています。 application_helper.rb def nofollow_link_to (body, url, options = {}, &block) url = url_for(url) html_options = options.reject { | k , _v | k == :context } nofollow = NoindexManager .new(url, options[ :context ]).noindex? ? ' nofollow ' : nil html_options[ :rel ] ||= nofollow if block.present? link_to(url, html_options, &block) else link_to(body, url, html_options) end end application_helper.rbに一度定義すると、ビューにおいてlink_toと同様に使うことができます。 index.html.slim ul.items - @items.each |item| li.item = nofollow_link_to(item.name, item.url, {context: item.count}) クロールディレクティブの正確性を担保する仕組み クロールディレクティブの設定ミスを防止するために、クロールディレクティブをテストできる仕組みを整備しました。IQONではcontrollerテストで出力HTMLを解析することで、クロールディレクティブの設定に問題ないかテストしています。クロールディレクティブの確認にユーザーアクションは不要なので、Selenium等を用いたクライアントテストは実施していません。 これから、実際にnoindexやnofollow、canonicalをどのようにテストしているのかをご紹介します。 ページのnoindex設定のテスト noindexのテストでは、まず出力HTMLからname="robots"を持つmetaタグを抽出します。そして、その抽出されたタグのcontent属性に「noindex」「nofollow」という文字列があるかどうかを判定します。判定のためのメソッドをspec_helper.rbに定義しておくことで、各controllerのテストでnoindexやnofollowの設定を確認できるようになります。 spec_helper.rb def meta_robots meta_tag = response.body.slice( /( <meta [^ > ]+? name="robots" .+?\/ > )/ ) content = meta_tag.slice( /(?<= content=" )(.+?)(?= " )/ ) index = content =~ / noindex / ? :noindex : :index follow = content =~ / nofollow / ? :nofollow : :follow { index : index, follow : follow} end item_controller_spec.rb describe ' GET :index ' do context ' meta robots content is `noindex,follow` ' do it do response_robots = meta_robots expect(response_robots[ :index ]).to be_falsey expect(response_robots[ :follow ]).to be_truthy end end end ページのcanonical設定のテスト canonicalのテストもnoindexのテストと同様に行っています。 spec_helper.rb def meta_canonical link_tag = response.body.slice( /( <link [^ > ]+? rel="canonical" .+?\/ > )/ ) link_tag.slice( /(?<= href=" )(.+?)(?= " )/ ) end item_controller_spec.rb describe ' GET :index ' do context ' meta canonical content is `https://item.iqon.jp/1000000/` ' do it { expect(meta_canonical).to eq( ' https://item.iqon.jp/1000000/ ' ) } end end まとめ noindex設定ロジックの共通化や、nofollowを考慮できるリンク生成ヘルパーの導入により、noindex・nofollow設定を効率よく管理できるようになりました。 また、クロールディレクティブをテストできる仕組みを導入したことにより、クロールディレクティブの正確性を担保できるようになりました。 最後に VASILYでは新技術でWebサービスを進化させたいエンジニアを募集しています。 興味がある方は以下のリンクをご覧ください。
アバター
同僚に3ヶ月のディープラーニング禁止令を言い渡したデータサイエンティストの中村です。 VASILYではスナップ画像に写っているモデルさんが着ている服と似ている服を検索する画像検索エンジンを開発しています。 ファッションアイテムを探す際、デザイン(アイテムの色や模様)はとても重要なファクターになります。 ファッションアイテムの画像検索システムも当然、色や模様のような局所的な特徴を捉えた検索を提供する必要があります。ところが判別タスクにおける歴代チャンピオンモデルと同様のCNNを使って特徴抽出を行うと、局所的な特徴が失われて似ていないアイテムがヒットしてしまうという問題がありました。 そこで、局所的な特徴の保存と表現能力の向上を期待して、モデルに浅いネットワークを追加してマルチスケールに拡張しました。 今回はこの取り組みについて紹介したいと思います。 スナップ画像から商品画像を検索する スナップ画像から商品画像を検索する方法については過去にもテックブログで紹介しています。 tech.vasily.jp 検索クエリはスナップ画像をトリミングした画像、検索対象はECサイトにあるような商品画像です。両者は性質が異なるため、ドメインを跨いだ画像検索となります。 このようなタスクにはCNNとTriplet Lossを用いた学習が有効であるというのが上記ブログの主張です。 局所特徴量の損失 画像認識におけるCNNの性能は今更紹介するまでもありません。CNNを使って抽出した特徴量は判別タスク以外にも様々な用途が存在します。もちろん検索にも有効なのですが、ファッションアイテムの画像検索に限定すると、判別タスクと同じモデルで抽出した特徴量はかえって精度を悪化させる場合が存在すると考えています。 判別タスクと異なり、ファッションアイテム検索において画像の局所的な特徴は重要な情報です。 例えば、服を買おうと思っている人の中に「カテゴリがシャツであればなんでもいい」と考える人は少ないと思います。人によって選ぶポイントは様々ですが、条件に色や模様が含まれる場合は多いと思います。 色や模様といった単純な構造は、本来1回フィルタを畳み込むだけで抽出できるのですが、CNNで何回もフィルタを畳み込むうちに失われてしまうと言われています。 カテゴリ判別等に有効な複雑な構造ももちろん大事なのですが、ファッションアイテム検索用のネットワークでは同時に単純な構造も抽出できる能力を持たせる必要があります。 マルチスケールなCNN Wang2014 *1 を参考に、より表現能力の高い特徴を抽出する能力を期待してモデルに浅いネットワークを追加し、マルチスケールに拡張しました。 浅いネットワークの仕組みは以下のとおりです。 図の中央のパスの入力は、元の画像(224x224 x 3)を57 x 57 x 3に解像度を落とした画像です。これにフィルタを畳み込んだ後に最大プーリングによって4 x 4 x 96のテンソルにします。 下のバスも同様の操作を施しますが、入力画像は更に解像度を落として29x29x3とします。 実装上は低解像度の画像を入力するのではなく、平均プーリングを使って解像度を落とします。 ネットワークの定義は以下の様になります。 class MultiscaleNetHingeTriplet (chainer.Chain): def __init__ (self, margin= 0.2 ): self.margin = margin self.train = True self._layers = {} # 深いネットワークの定義 # self._layers['conv1_1'] = ... # 浅いネットワークの定義 self._layers[ 'conv2_1' ] = L.Convolution2D( None , 96 , 3 , stride= 4 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 3 )) self._layers[ 'norm2_1' ] = L.BatchNormalization( 96 ) self._layers[ 'conv3_1' ] = L.Convolution2D( None , 96 , 3 , stride= 4 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 3 )) self._layers[ 'norm3_1' ] = L.BatchNormalization( 96 ) # 合流後の層 self._layers[ 'fc4_1' ] = L.Linear( 4096 , 4096 ) self._layers[ 'fc4_2' ] = L.Linear( 4096 , 256 ) super (MultiscaleNetHingeTriplet, self).__init__(**self._layers) forwardは画像を引数に取ります。 def forward (self, x): test = not self.train # 深いネットワークのforward # h1 = self.conv1(x) # ... h1 = F.normalize(h1) # 深いネットワークの出力は正規化する # 浅いネットワークのforward # 平均プーリング操作によるdown sampling h2 = F.average_pooling_2d(x, 4 , stride= 4 , pad= 2 ) h2 = F.max_pooling_2d( F.relu(self.norm_s1(self.conv_s1(h2), test=test)), 5 , stride= 4 , pad= 1 ) h3 = F.average_pooling_2d(x, 8 , stride= 8 , pad= 4 ) h3 = F.max_pooling_2d( F.relu(self.norm_s2(self.conv_s2(h3), test=test)), 4 , stride= 2 , pad= 1 ) h23 = F.concat((h2, h3), axis= 1 ) h23 = F.normalize(F.reshape(h23, (x.data.shape[ 0 ], 3072 ))) h = F.concat((h1, h23), axis= 1 ) h = F.normalize(F.relu(self.fc4_1(h))) h = self.fc4_2(h) return h 実験 浅いネットワークの効果を検証します。深いネットワークにはGoogLeNetを使いました。 深いネットワークにはVGGやResNetも使えますが、モデルパラメータ数と学習の簡単さという点でGoogLeNetを選びました。 余談ですが、モデルパラメータ数が増えると当然学習の難易度も上がります。データや計算リソースが限られている環境で実験を行う際はGoogLeNetのような軽量なネットワークから始めるのがおすすめです。 歴代チャンピオンモデルのパフォーマンス比較はCanziani2016 *2 がとても参考になります。 結果 上段は深いネットワークのみの検索結果、中段は浅いネットワークのみ、下段は浅いネットワークと深いネットワークを組み合わせたモデルの結果です。 浅いネットワークが入ったモデルは、クエリ画像のアイテムと似た色のアイテムを探す能力があることがわかります。マルチスケールなCNNの検索結果はアイテムの大まかな形状もクエリ画像と似ており、検索結果の分散も他のモデルと比べて小さいように見えます。 定量評価 Street2Shopデータセット *3 で簡単な実験を行いました。train/testを9:1で分割して全カテゴリ混ぜて学習させました(本当はカテゴリ毎に学習させたほうが良いです)。評価は幾つかのカテゴリについてRecall@5とnDCG@5を計算しました。 なお、検索には近似近傍探索を使っているため、値はあくまで参考値です。 Recall@5 Arch bags tops dresses skirts deep 0.35 0.38 0.26 0.79 shallow 0.50 0.33 0.29 0.83 deep+shallow 0.52 0.35 0.30 0.81 nDCG@5 Arch bags tops dresses skirts deep 0.26 0.28 0.18 0.60 shallow 0.36 0.27 0.20 0.66 deep+shallow 0.42 0.27 0.22 0.68 まとめ モデルに浅いネットワークを追加することで検索結果が改善しました。浅いネットワークだけでも簡単な特徴なら抽出できること、深いネットワークと組み合わせることで特徴量の表現能力が増すことを確認しました。 今後は事前学習や誤差関数を高度化することでより高精度な検索を達成していきたいと考えています。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 *1 : Jiang Wang, Yang Song, Thomas Leung, Chuck Rosenberg, Jingbin Wang, James Philbin, Bo Chen, Ying Wu. Learning Fine-Grained Image Similarity with Deep Ranking. In Proc. CVPR. 2014. *2 : Alfredo Canziani, Eugenio Culurciello, Adam Paszke. An Analysis of Deep Neural Network Models for Practical Applications. arXiv. 2016. *3 : M. Hadi Kiapour, Xufeng Han, Svetlana Lazebnik, Alexander C. Berg, and Tamara L. Berg. Where to Buy It: Matching Street Clothing Photos in Online Shops. In Proc. ICCV. 2015.
アバター
こんにちは。iOSエンジニアの遠藤です。 今回はiOSチームでの実装規約について紹介したいと思います。 Swiftのコーディングについてだけではなく、実装する上での細かい約束事をまとめました。 参考になれば幸いです。 実装規約について VASILYでのiOSアプリ実装規約は こちら からご参照ください。 実装規約とは? 普段多く見る規約はコーディング規約だと思います。 しかしVASILYではコーディングだけではなく、Interface Builder上でのViewの階層やコードの並び順などコード自体の書き方だけではなくチームで開発・実装をするうえで気をつけることについても触れています。 そのため、コーディング規約ではなく実装規約としています。 実装規約の目的 実装規約にも書いてありますが、本規約の目的は以下の3つです。 コードの統一 パフォーマンスの向上 メンテナンス性の向上 複数人で開発をしていると、スペースの空け方1つをとっても個人によって違います。 コードに統一性がないと様々な書き方が溢れてしまい、開発する上でもメンテナンスする上でも大変になってしまいます。 そのため、コードの統一性をとても重要視しています。 また、規約を守ることによりPRでの書き方の指摘も減り、本質的なレビューに注力することができるのも大きなメリットです。 誰が読んでも分かりやすいコードを書くために、思いやりのあるプログラミングが大事です。 実装規約の内容 短いguard節は一行で書く guard節のelseになった場合の処理は何もせずに return することが多いので、一行で書くようにしています。 コードの行数が短くなってスッキリとするのでコードの読み手に優しくなります。 VASILYでは一行100字を目安にしています。 例) // Bad guard array.isEmpty else { return } // Bad // Good guard array.isEmpty else { return } MARK, TODO, FIXMEを積極的に使用する ファンクションメニューにコメントが表示されるようになり、コードの管理がしやすくなります。 VASILYでは以下のような意味合いで使い分けています。 キーワード 内容 MARK 処理のまとまり TODO タスク FIXME 動作はしているが書き直したいコード 処理のまとまりや、タスクなどが明示的に書いてあることで開発するうえでもメンテナンスをする上でもコードやタスクの把握がしやすくなるためチーム全体に優しくなります。 例) final class TableViewController : UIViewController { // FIXME : スペースの実装が複雑なのでcollectionViewに置き換えてUICollectionViewFlowLayoutで実装する @IBOutlet private weak var tableView : UITableView ! override func viewDidLoad () { super .viewDidLoad() setupTableView() } // MARK: - View private func setupTableView () { tableView.delegate = self ... // TODO : あとでtableHeaderViewの設定をする } } // MARK: - UITableViewDataSource extension TableViewController : UITableViewDataSource { ... } Interface BuilderのDocument Outlineの順番 Interface BuilderのDocument Outlineの階層は見た目の階層と合わせるようにしています。 ぱっとみてどのパーツと紐付いているのかが分かりやすくなります。 例) // Bad // Good 実装規約を守るために 実装規約を守るには普段から意識してコードを書くことが大切です。 しかし、意識をしていたとしても規約を完璧に守るのは難しいです。 ですから、iOSチームでは以下の仕組みを利用して規約を守っています。 SwiftLint コードスニペット .clrファイルを共有する SwiftLint 実装規約を意識して書いていたとしても、うっかり余計なスペースが入ってしまうことはあります。 そのちょっとしたミスを見つけるためにビルドするたびにSwiftLintを使用してコードをリントをかけてチェックしています。 ビルド時にミスを見つけることができるので、PRで書き方の指摘を減らすことができます。 SwiftLint については以下を参照してください。 https://github.com/realm/SwiftLint コードスニペットの活用 UITableViewDataSource,UITableViewDelegateはアプリ開発で頻繁に実装するコードなので、コードスニペットを使用しています。 そうすることでコードの並び順を気にする必要がなくなります。 スニペット例 // MARK: - UITableViewDataSource extension < #viewController# > { override func tableView (_ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return < #code# > } override func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { < #code# > } } // MARK: - UITableViewDelegate extension < #viewController# > { override func tableView (_ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) { < #code# > } override func tableView (_ tableView : UITableView , heightForRowAt indexPath : IndexPath ) -> CGFloat { < #code# > } } .clrファイルを共有する Interface Builderで使用する色を.clrファイルにまとめてチームで共有しています。 そうすることで、微妙な色の数値間違いの発生を防ぐことができます。 また、設定したい色を簡単に選ぶことができるのも大きなメリットです。 さいごに iOSアプリの実装規約について紹介しました。 実装規約はチームで開発をしやすくするためのものなので、モダンな記法、分かりやすい書き方があれば随時更新していきます。 ちょっとした手間で優しさと思いやりある開発をしていきたいと思います。 VASILYでは優しさあふれるコードを書きたいエンジニアを募集しています。 少しでも興味がある方は以下のリンク先をご覧ください。 https://www.wantedly.com/projects/88978 www.wantedly.com
アバター