株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

こんにちは。 インフラエンジニアの光野です。 先日の ブログ記事 でご紹介したとおり、弊社のクローラーは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
こんにちは! なんでもディープラーニングでやりたがる癖が抜けず、3ヶ月のディープラーニング禁止令を言い渡されていた後藤です。 本記事ではVASILYで利用しているデータ分析の環境について紹介します。 VASILYではデータ分析が必要な場面で、BigQueryとTableauを組み合わせて利用することが多いため、これらの実際の活用例とTableauの選定理由について紹介したいと思います。 以前、CTOがデータ周りの環境の全体像を紹介しました。 tech.vasily.jp 社内ではBigQueryを中心にデータ周りの環境が構築されており、そこからデータ活用のあらゆる業務へつながります。 データの可視化と社内への共有は主にTableauを使っています。 まずは、BigQueryとTableauの説明から始めます。 BigQuery とは BigQueryとは、Googleが提供しているデータウェアハウスです。 BigQueryは強力なコンピュータリソースを使って、ギガスケールの巨大なテーブルの集計や結合を数秒〜数十秒で安価に行うことができます。VASILYでは、マスターデータやアクセスログ、クローラーのログなど、あらゆるデータがBigQueryに同期され、分析に必要なデータはBigQueryにクエリを投げることで取得できるようになっています。 VASILYにおけるBigQueryへのデータの同期方法は、前回のブログで軽く触れています。詳細については後日公開する予定です。 tech.vasily.jp Tableau とは Tableauは、様々な形式のデータソースから必要なデータを抽出し、ドラッグ&ドロップなどの簡単な操作のみで、データの可視化が素早く行えるBIツールです。VASILYでは、分析やダッシュボードの作成を行うデータサイエンティストがTableau Desktopを使い、ダッシュボードの閲覧がメインの他のメンバーにはTableau Serverでの閲覧権限を発行する、というように使い分けています。 Tableau Desktop デスクトップにインストールするタイプのTableauです。BigQueryにクエリを投げたり、ローカルのcsvファイルを開いて手軽に分析を始めることができます。 大きなデータの集計をBigQueryに任せてしまえば、これを導入するだけでデータを可視化できる便利な分析環境が整います。 Tableau Server Tableau Desktopで作成したダッシュボードをTableau Serverにパブリッシュすることで、ブラウザ上で閲覧できるようになります。他部署への情報共有はTableau Server上で行います。ほかにもクラウド上で完結するTableau Onlineや作ったダッシュボードを一般に公開できるTableau Publicなどがあります。 BigQuery + Tableauの活用例 BigQueryの強力な集計機能とTableauによる直感的な操作による可視化により、データ分析の時間が大幅に削減されます。 この2つを組み合わせたツールをVASILYでは以下のような場面で活用します。 探索的データ解析 仮説検証 KPIの監視や施策の効果測定 データの集計と共有 VASILYにとって貢献度の高かったBigQuery + Tableauの活用例を紹介します。 一部のviewに関してはTableau publicに公開し、Tableauでデータを探索する雰囲気を感じられるようにしました。 データのスライシング IQONのコーディネートに付けられたタグとコーディネートに使われたブランドの関係を期間別に把握したいという要望があった場合、以下のようなダッシュボードを作成します。複数のディメンジョンのあるデータを一枚の図で可視化することは難しいですが、Tableauであれば以下のようなダッシュボードによってインタラクティブにグラフを操作することができます。 このダッシュボードを使うことで、「ガーリー」というタグが使われたコーデに、どのようなブランドのアイテムが使われているのかをひと目で把握することができます。逆に、各ブランドはどのようなタグのコーディネートに使われる傾向にあるかを把握するためのダッシュボードも同様に作ることができます。このようなダッシュボードがあれば、営業的にはこれらの知識をいつでも引き出すことができ話題のタネとなりますし、データサイエンティスト的にはデータの傾向を大まかに把握することで、どんな分布を持っているか、どんなモデリングできそうか、どのような前処理が必要か、などの判断ができるようになります。調べたい仮説がある場合も、すばやく答えに行き着くことができます。 BigQueryの課金状況を可視化 以下のダッシュボードは各チームが日毎にどれくらいの価格のクエリを発行しているのかを可視化したものです。BigQueryは従量課金制であるため、下手なクエリを投げ続けると無駄な課金が発生してしまいます。このようなダッシュボードで調査することで、無駄なクエリを発行しているかどうかを把握することができます。 以下のダッシュボードは、クエリ毎の料金を面積の大きさで表現したものです。BigQueryの課金額を個人ごとに突き詰めていきます。誰がいくら分使い、それが全体のどれくらいの割合を占めるのかを把握することで、節約意識が芽生えるとともに、明らかに無駄なクエリを指摘することができるようになります。ここで問題になったクエリは精査され、テーブルを読み出す回数を減らす工夫をしたり、不要な列を読まないようにすることでコストカットを目指します。 アクセス数の分布 アクセス数の規模を面積の大きさに、アクセスあたりの利益を色の濃度にして可視化した例です。このダッシュボードにより、どの項目がアクセスの規模の割に効率的に利益を生み出しているのかがひと目でわかります。例えば、図の白い部分は、利益を生み出していない項目です。この面積が大きい場合、アクセスの割に利益を生み出していないことを意味します。現状の問題点や打ち手を考えるきっかけとなるダッシュボードなので重宝しています。 現在、VASLYではこのようなダッシュボードが200以上作られ、刻々と変化するVASILYの重要な指標を把握できるようになっています。 Tableauを選んだ理由 様々な場面で利用しているTableauですが、より安価に同じ機能を実現できるツールがないかを調査してみました。より安価になりそうなBIツールとしては以下のものが候補にあがりました。 re:dash exploratory google data studio Power BI Quick Sight Superset 候補の中からVASILYの環境にあるものを改めて調査した結果、結局Tableauがよいという結論になりました。VASILYが重視している要素は「BigQueryとの連携ができる」「バグが少ない」「探索的データ解析ができる」ことです。 BigQueryと連携ができる VASILYのデータ周りの環境はTableauを抜きにしてもBigQueryが欠かせません。アイテムやブランドのレコメンドやランキング、類似画像検索を実現する上でもBigQueryを活用しています。可視化したいデータもサイズが大きい場合が多いので、BigQueryの強力な集計機能を利用したいです。 上記のリストの中では、BigQueryとの連携ができるものとして、Tableau、re:dash、Exploratory、Google Data Studioなどが挙げられます。 一方で、PowerBI, QuickSight, SupersetなどはBigQueryが利用できないため、候補から外しました。 バグが少ない 意思決定に使う分析結果は間違っていないことと常に動き続けることが不可欠です。 BigQueryとの連携がお手軽なre:dashは、OSSでバグが多めということもあり今回は候補から外しました。 また、これはバグではありませんが、ExploratoryとGoogle Data StudioはTableauと同じ感覚で大きなデータを渡すと動かなくなる場面が多く、候補から外しました。 探索的データ解析がしやすい Tableauも簡単な探索的データ解析であればゴリゴリ進めることができますが、このリストのなかではExploratoryも使いやすいです。Rがベースになっているだけあって高機能で、UIからk-means clusteringのようなアルゴリズムも走らせることができます。VASILYではそこまでの高度な解析が必要になる場合、Jupyter Notebookを使います。 運用 BigQuery + Tableauの組み合わせで分析業務を回して、1年以上経ちます。その中で得られた運用の知見を述べます。 ダッシュボードの乱立を防ぐ Tableauを使うと、あまりにも手軽にviewが作れるため、他部署からたくさんの依頼が来るようになりました。我々も必要だと思い、ダッシュボードを大量に作りましたが、結局閲覧されないダッシュボードも乱立してしまい、かけた時間が無駄になることがありました。そこで、作成前にダッシュボードの必要性をデータチームで検討し、本当に知りたいことはなにか、かける時間に対して価値のあるダッシュボードになるかどうかをディスカッションしてから作り始めるようにしました。他部署の作業時間を大幅に減らせる見込みのあるダッシュボードのみを作成するようにしています。いまでは無駄なダッシュボードは一切作られず、データチームがダッシュボード作成にかける時間も減りました。 定期的に見るものはSlackに投稿する 業務に関係のある数値は日頃から追う必要がありますが、毎日Tableau Serverにアクセスしてチェックするのは負担です。そこで、重要な指標は自動的にSlackに投稿し強制的に目に触れるようにしています。毎日ほぼ強制的に数値を閲覧させられるため、大体の数値の感覚と、前日までの状態の良し悪しがわかるようになります。会社の飲み会で行ったクイズ大会で、KPIを答えさせる問題が出題されましたが見事に全員が答えられており、成果は出ていると感じています。 迷ったらサポートに聞く 保守費用を払っているため、カスタマーサポートを利用することができます。作業中にバグが出て作業が進まない場合は、悩まずにサポートに連絡して対応してもらいます。早ければその日のうちに回答がもらえ、なにかしらの行動が取れるようになります。 テクニックは先人に学ぶ Tableauをマスターし自在に使いこなせる人々のことをTableau Jediと呼ぶそうです。こちらからJedi達による美しいダッシュボードが閲覧&ダウンロードすることができます。 public.tableau.com ダッシュボード作成の際、やりたい操作が見つからない場合は、Tableau communityに質問を投げかけると、上級者がサンプルを作って見せてくれることもあります。かつて似たような問題に困っていた人の知見なども集められており、参考にすることが多いです。 最後に VASILYには分析しきれていない面白いデータが大量に眠っています。 我こそは思った方、データサイエンティストとして我々と切磋琢磨しませんか?
こんにちは、バックエンドエンジニアの塩崎です。 先日、VASILYバックエンドチームにインターン生が来てくれました。 この記事では彼がインターンで作ってくれた機能や、インターン中のスケジュールなどを紹介します。 インターンに来たのはこんな学生 インターンに来たのはこの春に大学4年生になったばかりの、柴犬大好き系エンジニアのT君です。 好きな言語はClojureというなかなかギークな学生さんでした。 インターンに来てもらう前に提出してもらった事前課題では、コードの綺麗さが光っていました。 この課題はRuby on Railsで「とあるお題」に従ったWeb APIを作るものなのですが、Rubyでありながら変数の再代入を嫌う傾向はさすが関数型に慣れているだけのことはあるなと感じました。 やってもらったタスク 目標 今回のインターンでT君にやってもらったタスクはMySQLからBigQueryのデータ同期の高速化・安定化です。 VASILYでは分析用データベースにBigQueryを使用しており、MySQLに保存されているマスターデータを毎日BigQueryに同期しています。 この処理を行うシステムは約2年前に書かれたもので、プロダクトの成長に伴うデータ量の増加に耐えることができなくなり始めていました。 BigQueryに保存されたデータはVASILYの機械学習システムの入力データとして使われ、ユーザーさんへの価値を届けるために使用されます。 また、会社としての意思決定を行うための資料であるダッシュボードの情報はBigQueryに保存されたデータを可視化したものです。 そのため、この同期処理が正しく行われている状態を維持することは非常に大切なことです。 彼にはこの同期処理を行うシステムを一から作り直してもらいました。 日程 T君には10日間インターンとしてVASILYで働いてもらいました。 技術調査から始めて、必要な機能の実装、本番環境へのデプロイまでをこの期間で行ってもらいました。 そして最終日には役員の前での成果発表会を行いました。 彼がどのようなスケジュールでインターンのタスクをこなしたのかを紹介します。 1〜3日目 データの同期処理そのものはembulkを使用することになりましたので、インターンの前半部分は技術調査をしてもらいました。 embulkが今回の用途に対して十分な機能を持っているかどうか、性能は十分かどうかなどの調査をしてもらいました。 具体的には絵文字(特にUTF8で表現した時に4byteになるような絵文字)を問題なく同期できるかどうか、数十GBの巨大なテーブルを同期する時の処理時間などを調査をお願いしました。 また、embulkのプラグインを調べてもらう中で、プラグインのドキュメントの不備を見つけていたので、その修正のPullRequestを出すようにそそのかしました。 彼にとっての初OSSコントリビュートだったそうです。 4〜6日目 インターン中盤にembulk単体では目標を達成できないことが分かったため、embulkのラッパーを作ることになりました。 embulkは1つのテーブルの同期設定を1つのYAMLで管理するため、複数個のテーブルを同期する場合にはこのYAMLファイルの管理が課題になります。 また、同期に失敗した場合のエラー通知や、同期のログを保存する機能もembulkそのものにはありません。 これらの課題を解決するためのラッパーを彼に書いてもらうことにしました。 VASILYのサーバーサイドは主にRubyが使用されており、Rubyに詳しいエンジニアが多いので、とりあえずRubyで書いてみることを勧めてみました。 ですが、Goで書いてみたいという彼の意見を採用し、Goでラッパーを書いてもらいました。 メンターである僕自身もGoにそこまで詳しいわけではなかったので、この瞬間からタスクがメンターにとってもチャレンジングなものに昇華しました。 彼は自発的にGoに関する資料を調べ、必要なライブラリなどを導入してくれましたので、今から振り返ると、Goを採用して良かったと思います。 7〜9日目 インターンの後半部分ではCIやデプロイなどの運用に関する部分を作ってもらいました。 CircleCIでGoのテストを実行したり、CircleCIのコンテナ内でクロスコンパイルしたGoのバイナリをプライベートネットワークの中のEC2インスタンスに配置したりする部分を実装してもらいました。 これらは彼にとって未知の領域でしたので、いろいろと調べながら実装することが多かった様子でした。 10日目 最終日にはVASILYの役員の前で成果発表を行いました。 役員の一人であるCTOの今村はエンジニアである、VASILYの技術のプロフェッショナルですが、他の役員はそうではありません。 エンジニアではない人に対して自分が行った成果を説明することにやや戸惑う様子もありましたが、非常にわかりやすく説明できており、役員から好評をもらいました。 最終的にできたもの 最終的にインターンの成果物として出来上がったものを紹介します。 以下の図は彼の成果発表のスライドから抜粋したものです。 これらの機能の詳細は機会があればTECH BLOGで紹介したいと思います。 設定された時間になると、Goで書かれたembulkラッパーをcronが呼び出します。 このラッパーはMySQLから同期するべきテーブルのスキーマ情報を読み込み、embulkの同期設定ファイル(YAML)を生成します。 そして、そのYAMLファイルを引数にしてembulkを起動し、データの同期処理を行います。 また、BigQueryに同期する時にGCSにテーブルのダンプを置くことによって、同期の安定化・高速化を図っています。 図中のほうれん草の画像から下の部分はログ収集部分です。 詳細なログはテキストファイル形式でサーバー内に保存されます。 要約されたログはVASILYのログ収集基盤によって収集され、最終的にはS3とBigQueryに動作ログが保存されます。 このログ収集基盤については以下の記事で詳しく説明されていますので、興味のある方はご覧ください。 tech.vasily.jp 本人の感想 10日間のインターンは、とにかく楽しすぎる10日間でした。 初日にミドルウェアを色々使いたいと要望を出したところ、今回の課題をやることになり、やりたいことをインターン生にやらせてくれるという柔軟さに驚きました。 やりたいことと、課題がマッチしていたおかげで積極的に楽しく取り組むことができました。 開発では技術調査から、プログラミング、テスト、CI、デプロイまで一気通貫で取り組むことができました。 CIやデプロイまわりは普段あまり経験する機会がなかったのですごく勉強になりました。 技術調査中にメンターの塩崎さんに指導してもらいながら初OSSコントリビュートも果たすことができました。 これからどんどんコントリビュートしていきたいと思います! 会社はとにかく賑やかで、気軽に質問したり、話をしたりと楽しく過ごすことができました。 今回作成したものが実際に利用されているということで自信にもつながりました。 インターンの経験を活かしてどんどんレベルアップして、塩崎さんみたいなすごいエンジニアになりたいと思っています!! まとめ VASILYのバックエンドチームにインターンに来てくれたT君のおかげでMySQLからBigQueryへデータ同期を行うシステムがモダンなものになり、安定して動作するようになりました。 BigQueryに格納されたマスターデータはBIツールであるTableauによって可視化され、それに基づいて意思決定が行われます。 そのため、安定して同期が行えるようになったことは意思決定を正しく行う上でとても意味のあることでもあります。 VASILYのインターンは本番環境で動かすための機能を作ってもらうことが大きな特徴です。 そのため、インターン生を「お客様」としては扱いません。 本番環境のコードに触れる以上、インターン生も1人のプロフェッショナルであるという思いを持って指導を行います。 そんな環境でスキルを磨きたいという熱意に溢れる学生の皆様はぜひ以下のバナーからご応募ください。
iOSエンジニアの庄司です。 今回は開発中のアプリで使った UIFeedbackGenerator についてご紹介します。 UIFeedbackGenerator とは、iOS 10以降で利用できるHaptic Feedback (触覚フィードバック) のAPIです。 この記事の要約 一般的な UIFeedbackGenerator の使い方を紹介。 iOS Human Interface Guideline でどのように推奨されているか解説。 自分はこんな場面で導入してみました。 UIFeedbackGenerator によるフィードバックを簡単に実装できる ライブラリ を公開しました。 Haptic Feedback とは 「触覚フィードバック」と訳します(今後 "Haptic Feedback" と呼びます)。 iPhone 6s 以降に搭載された Taptic Engineというハードウェアによる振動で、ユーザーのアクションに対するフィードバックを表現します。 Haptic Feedback の例 iPhone 7のホームボタンを押した時の振動 ホーム画面のアプリアイコンを強めに押してQuick Actionsが開く時の振動 UISlider のスライダーが両端にぶつかった時の小さな振動 UISwitch の ON / OFF を切り替えた時の小さな振動 UIFeedbackGeneratorの使い方 表題の UIFeedbackGenerator は抽象クラスで、Haptic Feedbackを発生させる実クラスは UIImpactFeedbackGenerator , UISelectionFeedbackGenerator , UINotificationFeedbackGenerator の3つです。 下記の表にそれぞれ特徴をまとめました。 UIFeedbackGenerator のサブクラス 種類 用途 UIImpactFeedbackGenerator 3種類 UIImpactFeedbackStyle ( .light , .medium , .heavy ) UIの衝突やスナップを表現します UISelectionFeedbackGenerator 1種類のみ 一連の離散値を動きを伝えます UINotificationFeedbackGenerator 3種類 UINotificationFeedbackType ( .success , .warning , .error ) タスクやアクションの成功/警告/失敗を通知する 実装方法 例) UIImpactFeedbackGenerator class ViewController : UIViewController { private let feedbackGenerator : Any ? = { if #available(iOS 10.0 , * ) { let generator : UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style : .light) generator.prepare() return generator } else { return nil } }() @IBAction private func light () { if #available(iOS 10.0 , * ), let generator = feedbackGenerator as ? UIImpactFeedbackGenerator { generator.impactOccurred() } } この先のiOS Human Interface Guidelineでも説明していますが、Haptic Feedbackの実行は遅延することがあるため、Haptic Feedbackと同時に音声を流すときなど、実行前の適切なタイミングで UIFeedbackGenerator の prepare() メソッドを呼ぶことが推奨されています。 prepare() を呼んでおくことで数秒間準備状態になり、Haptic Feedback発生のレイテンシーを下げることができます。 prepare() のタイミングが早すぎたり、Haptic Feedback発生と同時に呼んだ場合は効果がありません。 prepare() メソッドを呼ぶことはオプションですが、 ドキュメント では強く推奨されています。 iOS 10以降のAPIのため、iOS 9以下のOSをサポートする際は、 UImpactFeedbackGenerator のプロパティを生成するタイミングと、Haptic Feedbackを発生させるタイミングで、iOSのバージョン判別が必要になります。 Haptic Feedbackの使いどころ iOS Human Interface Guidelines に Haptic Feedback を実装する上での注意点がまとめられています。 下記はそのガイドラインの意訳と解説です。 ここでいうフィードバックとは、アクションの結果をユーザーにフィードバックする手段です。 アラート表現はフィードバック方法としては強力ですが、重要な情報を含まないアラートを多用していると、そのうち重要な情報も含めてアラートそのものを無視するようになります。 そこでHaptic Feedbackを使いましょう。 ユーザーに注意喚起してアクションを強化する物理的フィードバック(振動)を発生させます。 Success. 入金成功や車の解錠成功を伝えます。 Use haptics judiciously. フィードバック全体の意味が薄れてしまうので、多用は避けましょう。 In general, provide haptic feedback in response to user-initiated actions. ユーザーのアクションに合わせてフィードバックを発生させましょう。任意のタイミングでのフィードバックは意識を中断させ、誤解を生みます。 Don't redefine feedback types. フィードバックタイプを意図どおりに使用してください。タスクが成功したことをユーザーに通知するには、 UINotificationFeedbackGenerator の UINotificationFeedbackType.success を使用し、それ以外のものは使わないようにしましょう。 Fine tune your visual experience for haptics. 視覚と触覚のフィードバックを同時に発生させることでアクションと結果のより深いつながりを提供します。 例) 操作のエラーを表現する時に音と一緒にHaptic Feedbackを発生させる。 Don’t rely on a single mode of communication. 全ての端末がHaptic Feedbackをサポートしているわけではないので、アクションの結果をHaptic Feedbackだけにしてはいけません。 Use haptics when visual feedback may be occluded. オブジェクトをスクリーン上の場所にドラッグするときなどは、指で隠れてしまいます。特定の場所や値に到達したときにユーザーにHaptic Feedbackで通知することを検討してください。 Prepare the system before initiating feedback. Haptic Feedbackは遅延が伴うことがあります。フィードバックを発生させる直前にシステムを準備することが最善です。そうしないと、Haptic Feedbackが遅すぎて、ユーザーの行動や画面に表示されている内容から切り離されているように感じる可能性があります。 Synchronize haptics with accompanying sound. Haptic Feedbackは音を発生させません。Haptic Feedbackと同時に音が必要なら、各自で音声同期処理を実装する必要があります。 こんな場面で導入してみました 公開しているライブラリ RangeSeekSlider の、段階的に値を変えられるスライダーで、値が変わったことを伝えるためにHaptic Feedbackを発生させるようにしました。(前の項目では Use haptics when visual feedback may be occluded. に当たるものです。) 値が変わったタイミングで UISelectionFeedbackGenerator のフィードバックを呼び出しています。 下記のGIFの「Range with Step」のオレンジ色のスライダーです。 ライブラリ化しました TapticEngine と言うライブラリにして公開しました。 150行程度 の小さいライブラリです。 まだシェアが多く切ることができないiOS 9と同居して使えるようにしました。 OS判定の必要がありません。 Taptic Engine が搭載されていない端末、iOS 9端末では処理を呼び出しても何も動作しません。 // Triggers a impact feedback between small, light user interface elements. (`UIImpactFeedbackStyle.light`) TapticEngine.impact.feedback(.light) // Triggers a impact feedback between moderately sized user interface elements. (`UIImpactFeedbackStyle.medium`) TapticEngine.impact.feedback(.medium) // Triggers a impact feedback between large, heavy user interface elements. (`UIImpactFeedbackStyle.heavy`) TapticEngine.impact.feedback(.heavy) // Triggers a selection feedback to communicate movement through a series of discrete values. TapticEngine.selection.feedback() // Triggers a notification feedback, indicating that a task has completed successfully. (`UINotificationFeedbackType.success`) TapticEngine.notification.feedback(.success) // Triggers a notification feedback, indicating that a task has produced a warning. (`UINotificationFeedbackType.warning`) TapticEngine.notification.feedback(.warning) // Triggers a notification feedback, indicating that a task has failed. (`UINotificationFeedbackType.error`) TapticEngine.notification.feedback(.error) // Prepare a impact feedback for `UIImpactFeedbackStyle.light`. TapticEngine.impact.prepare(.light) // Prepare a selection feedback. TapticEngine.selection.prepare() // Prepare a notification feedback. TapticEngine.notification.prepare() 最後に 今回は、 UIFeedbackGenerator の使い方について紹介しました。公開したライブラリは小さなライブラリなので、気になることがあれば是非ソースコードを読んでPRを送ってください。 VASILYでアプリを作りながらOSS開発もやりたいiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。
こんにちは! バックエンドエンジニアのりほやんです! 2017年の2月28日にIQONはリブランディングを行い、タグラインを "わたしの「好き」がここにある” に刷新しました。 この “わたしの「好き」がここにある” という体験をユーザーにしていただくには、IQONに掲載されている商品情報がとても重要になります。 そして、正確な商品情報の掲載にはクローラーが正しく運用され稼働していることが必要不可欠です。 本記事では、IQONの商品情報を支えるクローラーの運用をどのように仕組み化しているかについてご紹介します。 クローラーを作成、運用されている方々のお役に立てたら幸いです。 弊社テックブログでは以前、『速くクロールすること』に注目した記事を公開しましたが、今回は『正しくクロールすること』に注目しました。 『速くクロールすること』に注目した 『Docker / Apache Mesos / Marathon による3倍速いIQONクローラーの構築』 を合わせて読んでいただくと、より深く弊社のクローラーを理解していただけると思います。 tech.vasily.jp IQONのクローラー運用 IQONでは、毎日数百の提携ECサイト、数百万ページのクロールを行っています。 そして、提携しているすべてのクロール元サイトから辿ることができる全ての商品ページを取得し、なおかつタイトル・価格・ブランド・在庫などのすべての情報が記載されている内容のまま、IQONに掲載されていなければいけません。 そこで、下記の2つの条件を満たすことをクローラーが正常稼働している条件としています。 正しい商品情報がクロールされている すべての商品がクロールされている VASILYではそれぞれの条件を満たすために、以下のことを行っています。 正しい商品情報がクロールされている 作成: リニューアルや修正に強いXPathを作成する 作成: 細かくパースチェックを行う 運用: パースエラーが発生していないかをチェック すべての商品がクロールされている 運用: 商品詳細総ページ数に変動がないかチェック 運用: 新商品の追加数に変動がないかチェック では実際に、それぞれのチェックや重要なポイントをクローラ作成時と運用時に分けてご紹介します。 クローラー作成 リニューアルや修正に強いXPathを作成する VASILYでは、クローラー作成時の要素パースにXPathを用いています。 XPathはXML文章中の要素、属性値などを指定するための言語です。 クローラー運用の負担を軽減させるため、クローラー作成時にリニューアルや修正にも強いXPathを作成することが大切です。 クローラー作成に便利なXPathの記法を2つご紹介します。 複数のclassが指定されている要素の取得 下記のようにclassが複数指定されている要素を取得したい場合のXPathを考えてみます。 // 商品タイトル < div class = "title class1 class2" > ロングカーディガン </ div > 上記は『ロングカーディガン』という商品タイトルを表すHTMLです。 ロングカーディガンという文字を取得したい場合、どのようなXPathになるでしょうか。 classを指定して要素を取得する場合、下記のようなXPathがすぐに思いつくと思います。 //div[@class='title class1 class2'] => ロングカーディガン しかし、リニューアルなどで紐付いているclassが以下のように変更された場合、先ほどのXPathでは要素が取得できなくなってしまいます。 // 商品タイトル < div class = "title class1 class3" > ロングカーディガン </ div > そこで、指定する文字列が含まれている要素を取得することができるcontainsを使います。 『ロングカーディガン』は商品タイトルなので、titleというclass名は変更されにくいと考えられます。 このように変更されにくいclass名を指定して、下記のように記述します。 //div[contains(@class,'title')] => ロングカーディガン このXPathは、titleというclassが含まれるdiv要素を取得します。 このXPathであれば、先ほどのように紐づくclassがclass2からclass3に変更されたとしても、titleというclassが指定されている限りタイトルの取得が可能です。 テーブル内の要素を取得する場合 次にテーブル内の要素を取得するパース処理を考えてみます。 < table class = "table" > < tbody > < tr > < th > カテゴリ </ th > < td > カーディガン </ td > </ tr > < tr > < th > 素材 </ th > < td > 綿100% </ td > </ tr > < tr > < th > 性別 </ th > < td > レディース </ td > </ tr > </ tbody > </ table > このようなテーブルからレディースという要素を取得したい場合、tr要素の3番目のtd要素という情報から下記のように書くことができます。 //table/tbody/tr[3]/td しかし、上記のようにtrの3番目というような指定をするとテーブルの要素が増減した際に、違う情報を取得してしまう可能性があります。 // 要素が増えてしまった時、trの3番目の要素は原産国になってしまう。 < table class = "table" > < tbody > < tr > < th > カテゴリ </ th > < td > カーディガン </ td > </ tr > < tr > < th > 素材 </ th > < td > 綿100% </ td > </ tr > < tr > < th > 原産国 </ th > < td > 日本 </ td > </ tr > < tr > < th > 性別 </ th > < td > レディース </ td > </ tr > </ tbody > </ table > //table/tbody/tr[contains(th,'性別')]/td => 原産国 そこで、tdに紐づくth要素を指定することで、要素の増減に左右されず性別を取得することができます。 //table/tbody/tr[contains(th,'性別')]/td => レディース 細かくパースのチェックを行う 要素をパースする処理を書き終えたら、正しいパースを行えているかを確認するために、パースチェックを行います。 パースのチェックスクリプトを作成しておくと便利です。 この際、下記の2つのチェックを行うスクリプトを用意します。 1つのパースをチェックする すべてのパースをチェックする すべてのパース処理を書いた後にすべてのパースをチェックするだけでなく、1つパース処理を作成する度にパースのチェックを行う方が、ミスに気づきやすく最終的に運用の負担が少なくなります。 このようなクローラー作成作業を効率化するために、VASILYでは、Crawler Generatorという社内ツールが開発されています。 このツールでは、クロールする項目ごとにXPathや、正規表現などを設定するだけでパース処理が作成できるため、効率的にクローラーを作成できます。 また、項目ごとにプレビュー機能がついており細かく項目のチェックを行うことで、間違った情報をパースしてしまっていることにもすぐに気づくことができます。 クローラー運用時 クローラーを作成した後も、クロール元サイトのリニューアルなどにより正しいデータ取得できなくなることがあります。 この正しいパースができなくなっているという異常を未然に防ぐことは難しいため、いち早く異常を検知し、修正することが大事です。 VASILYではクローラーの異常を検知するために、3つのチェックを行なっています。 パースエラーが発生していないか 商品詳細総ページ数に変動がないか 新商品の追加数に変動がないか パースエラーが発生していないか 初めは正しい情報をパースできていても、クロール元サイトのリニューアルなどで、正しい情報をパースできなくなることがあります。 そこで処理を行った商品ページ総数のうち、ある一定以上パースが失敗している場合を、異常として検知します。 しかし異常が検知されたクローラーの何のパースが失敗しているかを特定・調査しなければいけません。 この時、ログが散在していたり・使いにくい状態だと調査に時間がかかってしまいます。 そこで、ログの集約・調査に、BigQueryを用いています。 BigQueryにログ集約テーブルを作成し、クロールの処理ログを流すことで、エラーの特定調査を効率的に行えるようにしています。 あるサイトでパース異常が検知されたら、どこのパースが失敗しているかをBigQueryのログ集約テーブルから調査し、修正を行います。 商品詳細総ページ数に変動がないか IQONのクローラーは毎日クロール元の商品リストページから、全商品詳細ページを取得しています。 ECサイトの商品総数は日々多少の増減はしますが、大幅に変化することはあまりありません。 そこで毎日の総商品詳細ページ数を監視し、大幅な変化が見られるサイトを異常とみなしチェックすることで、すべての商品をクロールしていることを保証しています。 この大幅な変化が見られるサイトを検知するために、標準偏差を用います。 毎日クロールするページ総数が変わらないことを期待し、直近1週間のクロール数と今日のクロール数から、標準偏差を求め異常かどうかを検知しています。 ここで直近1週間のデータを使用している理由は、様々な日数で試した結果、異常がない日が続く日数として1週間が適切であったからです。 一週間以上の長い期間を対象とすると、過去に異常検出された日が入ってしまい、異常でないサイトも異常として検知されてしまうことがあります。 また、異常とみなす閾値については、初めは厳しく設定し運用する中で調整しています。 新商品の追加数に変動がないか 商品詳細総ページ数チェックに加えて、新商品追加数に変動がないかをチェックしています。 しかし新商品が毎日追加されるサイトもあれば、更新頻度が低いサイトもあり商品追加頻度は各ECサイトごとに様々です。 そこで、2つのチェックを行い異常を検知しています。 まず過去新規アイテム追加0である日が最大何日続いていたかを調べ、その連続日数を超えている場合、異常として検知するというチェックを行っています。 具体的には、直近1か月のクロール情報から日ごとの新規アイテム追加数を取得し、アイテム追加が0である日の最大継続日数を算出します。 今日のアイテム追加数が0であるサイトの中から、最大継続日数を更新しているものを異常とみなし検知します。 ここで直近1か月のデータを用いているのは、先ほどと同じく異常が正しく検知されるのに適した日数が1か月であったからです。 状況によって適した日数は変わりますが、だいたいの目安として考えていただけると幸いです。 また、毎日アイテム追加が行なわれているサイトが、急にアイテム追加されなくなった場合も検知しています。 具体的には、連続10日間アイテム追加されていたが、突然アイテム追加数が0になった場合を異常とみなして検知しています。 連続10日間アイテム追加があるということは基本的に毎日アイテムが追加されるサイトと考え、毎日アイテム追加があるはずのサイトが急にアイテム追加されなくなった場合は、こちらのクロールに異常があると考えこのチェックを導入しています。 過去10日間のデータを用いているのは、パースのエラーなどの要因でアイテム追加に影響があったサイトも、この検知の対象になるように調整した結果です。 修正対象クローラーを分かりやすく確認するために これまでIQONがクローラーをどのようなチェックで運用しているかをご紹介しました。 これらはチェックの方法であり、毎日どのサイトで異常が発生しているかを一目で確認できるものが必要です。 VASILYでは、昨年末まで修正対象のクローラーを検知するために専用のダッシュボードを作成し運用に使用していました。 しかし、誰がどのクローラーの修正を行ったかがわからなかったり、見るべき指標が多く、異常を検知できていない時があるという問題がありました。 そこで改めて効率的にクローラーの異常をわかりやすく検知し運用するためにはどうすれば良いかを考えた結果、クローラーチェックツールの開発に至りました。 このツールでは、運用に必要な下記3つのチェック項目それぞれについて正常か異常かが一目でわかり、なおかつ対応が必要なクローラーがどれかをすぐに確認することができます。 また修正状況を確認することもできます。 商品詳細ページ数に変動がないか 新規アイテム追加に異常はないか パースのエラーが発生していないか すべてのサイトに対して3つのチェックを行い、異常が検知されたチェックを赤く表示しています。 この時どれくらいエラーが起きているかという数値は表示せず、エラーが起きているサイトはすべてその日中に修正すべきと考え、異常か正常かという表示のみを行っています。 担当の欄には、バックエンドチームの名前がランダムに割り振られ、毎日割り振られた担当のクローラーの修正を行います。 異常が修正されたことを確認した後、画面上のFixedボタンを押すことで、対応完了したことが他の人にもわかるようになっています。 このツールを用いることで、どのクローラーで異常が発生しているかが一目でわかりまた誰がどのクローラーを修正したかが分かりやすくなり、効率的にクローラーを運用できるようになりました。 まとめ 以上、IQONのクローラー運用についてご紹介しました。 IQONでは、数百のECサイトをクロールしているため、クローラーを効率的に作成・運用するために仕組み化を行っています。 現在、クローラーの異常は当日中に検知・修正し、再稼働させていることで毎日正しい情報をIQONに掲載することができています。 ぜひご紹介した仕組みの中で参考になるものがあれば導入してみてください♩ 最後に クローラーの運用はとても地道です。しかしIQONに、商品情報が正しく掲載されていることは必要不可欠です。 そして正しい商品情報を掲載するためのクローラーを、効率的に運用することにとてもやりがいを感じます! VASILYでは、クロール・効率化・仕組み化に興味がある人を大募集しています。 春季インターンの募集も行っておりますのでご興味のある方は、是非弊社に遊びに来てください!
こんにちは、バックエンドエンジニアのじょーです。大規模なサービスのAPIを開発する際に、ルールを決めずに開発していると無秩序なコードが散見される運用がしづらいAPIになってしまいます。また、ルールを決めたとしても共有が上手くいかないなどの理由で守られなくなってしまうこともあると思います。 本記事では、APIを運用しやすくするために、ただルールを決定しただけではなく、ルールを守るためにそれぞれ仕組み化をしたことを紹介します。 APIのレスポンスを統一する デコレーターを使ってレスポンスの定義を綺麗に書く パラメーターを統一する Validatorによりパラメーターの明記を強制する コーディング規約を守る LinterとSideCIを導入して修正とレビューの自動化 Linterのルールを適度に調節する 1. APIのレスポンスを統一する ここで言うAPIのレスポンスを統一するというのは、返すAPIフォーマットのブレをなくすことです。フォーマットが一定のルールに従うことで使う側はもちろんのこと、作る側にもメリットがあります。 レスポンスを統一するのは当たり前のように感じますが、開発者が複数人いる際は細かい部分にブレが生じてきます。 そこで、APIレスポンスを統一するために実践しているテクニックを紹介します。 デコレーターを使ってレスポンスの定義を綺麗に書く APIを作る際に、DBのレコードをそのまま加工せずに返す例は少ないです。レコード自体に情報を足して返したり、別の情報を付加して返すことがほとんどだと思います。 その際、レスポンスを定義する処理をモデルに書いているとあっという間にモデルが肥大化してしまいます。場合によっては、同じような処理が乱立してレスポンスの統一が難しくなる可能性があります。 そのような場合、レスポンスを定義するコードはモデルとは別ファイルに切り出した方が処理が一箇所にまとまって見やすくなり、レスポンスを統一することも簡単になります。 弊社では、 draper というgemの Draper::Decorator と、 fieldgroup という、レスポンスのまとまりを定義する独自の手法を使ってモデルとは別ファイルに処理を切り出しています。 まず、 Draper::Decorator の処理を簡単に説明します。 Draper::Decorator を使うと、モデルをwrapして、レスポンスに付加したい情報を定義することができます。 下記は、紐づくショップの情報をブランドのレスポンスに追加したい場合の例です。 モデル名 + Decorator という命名規則のクラスを宣言し、 Draper::Decorator を継承して使います。 class BrandDecorator < Draper :: Decorator # ブランドモデルのレコードをwrapする記述 delegate_all # 付加したい情報の記述 # 'context'を外部から指定して受け取ることが可能 def shop Rails .logger.info ' shop情報ですよ ' if context[ :logger ] == true Shop .find(object.shop_id).attributes end end このようにクラスを定義すると、Brandモデルのインスタンスに .decorate メソッドを使ってアクセスしブランドの情報を引くことができるようになります。 また、 .decorate(context: { logger: true }) のように指定すると、メソッドでcontextの内容を受け取ることができます。 # もともとレコードに存在するキーはそのままレコードの内容を返す > Brand .find( 1 ).decorate.name => ' Lawrys Farm ' # Decoratorに宣言したキーはメソッドで定義した内容を返す > Brand .find( 1 ).decorate.shop => { id : 1 , name : ' 代官山店 ' } # contextを指定 > Brand .find( 1 ).decorate( context : { logging : true }).shop => ' shop情報ですよ ' => { id : 1 , name : ' 代官山店 ' } ( Draper::Decorator の機能の詳細は こちら を参照してください。) この Draper::Decorator の機能とプラスして、 fieldgroup という、レスポンスのまとまりを定義して自由にレスポンスのサイズを変えられる仕組みを独自で作っています。レスポンスのまとまりを1ファイルに定義することで、そのファイルを見るだけでどのようなレスポンスが返るかわかりやすくなったり、レスポンスのキーを足したり減らしたりすることが容易になります。 下記に、ディレクトリ構成と、実際の fieldgroup の実装内容を載せます。 ディレクトリ構成 app/decorators - item_decorator.rb - user_decorator.rb - brand_decorator.rb class BrandDecorator < Draper :: Decorator include FieldgroupDefinedable delegate_all define_fieldgroup :small , [ :id , :name , :kana , :initial ] define_fieldgroup :medium , [ :shop ] define_fieldgroup :large , [ :total_item_count ] def shop # 付加したいショップ情報を記述 Shop .find(object.shop_id).attributes end def total_item_count # ブランドにひも付くアイテム数を取得する処理 end end このように、大、中、小(large, medium, small)に分けてレスポンスを定義しています。 リストページなどの少ない情報を返す際にはsmall、詳細ページにはlargeを指定するなどしてシーンに合わせて使い分けることができます。 実際に中、小のグループを引いた例がこちらです。 # フィールドグループsmallを指定 > Brand .find( 1 ).decorate( context : { fieldgroup : ' small ' }).to_hash => { id : 1 , name : ' Lawrys Farm ' , kana : ' ローリーズファーム ' , initial : ' L ' } # フィールドグループmediumを指定 > Brand .find( 1 ).decorate( context : { fieldgroup : ' medium ' }).to_hash => { id : 1 , name : ' Lawrys Farm ' , kana : ' ローリーズファーム ' , initial : ' L ' , shop : { id : 1 , name : ' 代官山店 ' } } 上記では .decorate にプラスして、 .to_hash をつけています。 .to_hash をつけることで define_fieldgroup で定義した内容のまとまりがHashで返るような仕組みを独自開発しています。また、contextに fieldgroup を指定することで指定したサイズのレスポンスが返る実装になっています。 .to_hash でレスポンスをまとめて返す処理と、 define_fieldgroup の内部の詳しい実装は以下のようになっています。 module FieldgroupDefinedable extend ActiveSupport :: Concern included do class << self def define_fieldgroup (name, keys) @fieldgroup_keys ||= {} @fieldgroup_keys [name] = keys end def fieldgroup_keys (name) @fieldgroup_keys [name] end end # contextで指定されたfieldgroupのサイズを取得する処理 def hash_group_name context[ :fieldgroup ].present? ? context[ :fieldgroup ].to_sym : :large end # 各サイズで定義されたキーの内容を引いてきて、JOINする処理 def to_hash case hash_group_name when :small to_hash_fieldgroup( :small ) when :medium to_hash_fieldgroup( :small ) .merge(to_hash_fieldgroup( :medium )) when :large to_hash_fieldgroup( :small ) .merge(to_hash_fieldgroup( :medium )) .merge(to_hash_fieldgroup( :large )) end end def fieldgroup_keys (name) self .class.fieldgroup_keys(name) end private # Draper::Decoratorの機能を利用してfieldgroupに定義された各キーを引き、Hashに直す処理 def to_hash_fieldgroup (group_name) Hash [fieldgroup_keys(group_name).map { | k | [k, send(k)] }] end end end 上記のモジュールをデコレーターのクラスにincludeすることで、 .to_hash と fieldgroup を使って、レスポンスをまとめて返すことが可能になります。 to_hash_fieldgroup メソッドが少しトリッキーな手法を使っていますが、 Brand.find(1).decorate オブジェクトに対して、 define_fieldgroup に定義されたkey名と値にアクセスするためのメソッド名が同一なことを利用して値を取得し、Hashにしています。 このように、1ファイルにまとめることでどのような項目のレスポンスが返るかが見やすくなり、レスポンスの統一が容易になります。また、レスポンスの変更もスムーズに行うことができるようになります。 2. パラメーターを統一する APIのパラメーターを統一するというのは、同じ意味を持つパラメーターが違う命名で乱立するのを防ぐことや、受け取るパラメーターのルールをしっかり決めることです。 APIが受け取るパラメーターを統一することで、使う側にメリットがありますし、開発側もデバッグが楽になります。 そこで、パラメーターを統一するために実践しているテクニックを紹介します。 Validatorによりパラメーターの明記を強制する パラメーターが統一されない一番の原因は、現在受け取るパラメーターが何なのかコードを深く読まないとわからないことだと考えています。 ドキュメントを作成することが強制されていたり、ルールがしっかり決まっていればまだいいかもしれませんが、ドキュメントやルールがある場合でも、更新することを怠ってしまうと網羅的にパラメーターを把握できなくなります。 それにより、現状どのようなパラメーターが使われているかを知ることが大変になり、パラメーターにブレが発生します。 例えば昇順、降順を指定するパラメーター名が order と sort でブレてしまったり、それらが受け取る値が ASC DESC を受け取れたはずが小文字の asc desc しか受け取れないなどです。 そこで、全てのエントリーポイントに共通のValidatorを挟み、YAMLに記述のないパラメーターが渡された場合はエラーが出るようにしています。そうすることで、必ずYAMLに記述するという強制力が生まれます。 また、YAMLにパラメーターに関する細かいルールも一緒に記述できるようにしています。 下記が、実際のValidateの設定YAMLの例です。 YAMLはモデルと対になっています。 show: # アクション名 id: # 受け取るparameter type: Int # 型の指定 required: true # 必須 index: id: type: Int required: true initial: type: String limit: max: 100 # 上限値 page: order: within: ['ASC', 'DESC'] # 受け取る値の限定 アクションごとに受け取るパラメーターを列挙し、各パラメーターに対しての条件を記述しています。 受け取れる条件は下記の表のようになっています。 条件 定義 例 required 必須項目 true blank 不可 true format 正規表現でのフォーマット指定 /[<\=>]\s*\$\d+/ is 受け取る値の限定 1 within 受け取る値の限定(複数) ['DESC', 'ASC'] min 最小値 200 max 最大値 2000 min_length 最小長 200 max_length 最大長 2000 こうすることで、細かいルールも含めて現状のパラメーターを知ることができます。 パラメータ管理の手法として、 rails_param のようなgemも存在しています。 しかし、 rails_param だとコントローラーに設定を書かなければならないのでメソッドが長くなってしまうということ、パラメーター設定の記述に対する強制力が弱いことなどから今回は自前の実装にしています。Railsにもともと備わっている permit も同様にコントローラーに直接書かなければならないことや、書くことに強制力がないので今回は使っていません。 以下が実装例です。 # frozen_string_literal: true require ' yaml ' require ' exceptions ' module ValidateParams class InvalidParameterError < InvalidParameter attr_reader :param , :options def initialize (message, param = nil , options = nil ) @param = param @options = options super (message) end end module_function def validate_param (params) # YAMLを取得して設定を元にvalidate_inspectionをかける処理 validate_inspection(params, name, type, options = {}) end def validate_inspection (params, name, type, options = {}) return unless params.include?(name) || options[ :required ] begin param = coerce(params[name], type, options) validate(param, options) return if options[ :no_cast ] params[name] = param rescue InvalidParameterError => e raise InvalidParameterError .new(e.message, param, options) end end # YAMLで設定した型チェック処理 def coerce (param, type, options = {}) return param if param.is_a?(type) || param.nil? if [ Integer , Float , String ].include?(type) coerce_primitive_type(param, type) elsif type == Array delimiter = options[ :delimiter ] || ' , ' coerce_array(param, type, delimiter) elsif [ Date , Time , DateTime ].include?(type) coerce_datetime(param, type) elsif [ TrueClass , FalseClass , :boolean ].include?(type) coerce_boolean(param) else nil end rescue ArgumentError raise InvalidParameterError , " ' #{ param } ' is not a valid #{ type }" end # YAMLで設定した細かいValidate処理 def validate (param, options) options.each do | key , value | case key when :required validate_required(param, value) when :blank validate_blank(param, value) when :format validate_format(param, value) when :is validate_is(param, value) when :within validate_within(param, value) when :min validate_min(param, value) when :max validate_max(param, value) when :min_length validate_min_length(param, value) when :max_length validate_max_length(param, value) end end end --------以下各型チェック処理--------- def coerce_primitive_type (param, type) Kernel .__send__(type.to_s, param) end . . . --------以下各validate処理--------- def validate_required (param, value) raise InvalidParameterError , ' Parameter is required ' if value && param.nil? end . . . end こちらをapplication_controllerの最初に挟むことで、すべてのエントリーポイントにおいてValidatorが動作します。 class ApplicatonController < ActionController :: Base before_action do ValidateParams .validate_param(params) end end このようにしてValidatorを挟むことによりパラメーターを見える化し、次に新しいパラメーターを作る際に同じような役割のパラメーターが以前になかったかYAMLを見るだけで確認することができます。また、各パラメーターが持つルールも一目でわかるようになります。強制力が働いていることによって開発者に依存せずにパラメーターの可視化をすることができます。 3. コーディング規約を守る コーディング規約を定めて特定のプログラミング作法に従うことは、コードの可読性を高め、間違いを減らす効果があります。 しかし、 コーディング規約の自動化 にも述べられているように、規約をただ定めたとしても意識のみで守り続けることは難しいことです。 そこで、チームでコーディング規約を守るためにしたことを紹介します。 LinterとSideCIを導入して修正とレビューの自動化 Linterと SideCI を使って、コーディング規約に従って自動的にコードを修正したり、修正するべき内容をGithubのPullRequest上でチームにシェアできるようにしています。 まず、LinterとSideCIそれぞれについて少し説明します。 Linterとは、静的コードを解析してコードを正しい文法や推奨された書き方を指摘してくれるツールです。 弊社では、下記のLinterを導入しています。 linter名 デフォルトconfig 特徴 rubocop rubocop default config Ruby style guide に基いて作られたLinter reek reek default config 読みづらさや保守しづらいコードを指摘してくれるLinter brakeman brakeman default config セキュリティチェック系のLinter rails best practice rails best practice default config Railsのベストプラクティスを基にしたLinter 各Linterの導入は、詳しく書かれている記事が存在しているのでそちらを参照することをお勧めします。 下記のように、Linterにかけたいスクリプトを指定して、Linterのコマンドを叩くと自動的に修正がかかります。また、機械的に修正できない場合は修正すべき箇所を文章で指摘してくれます。 こちらはrubocopの修正をかけた際の例です。 5行目の指摘は、早期リターンを勧めてくれていますが、機械的に修正すると危険な項目なので指摘のみです。 6行目の指摘は、インデントの指摘を自動で修正してくれています。 このように、自動的にコードを見て問題箇所を指摘したり、修正してくれます。 次にSideCIがどういったサービスかについて少し説明します。 SideCIは、GitHubとLinterを利用したコードレビューを行うサービスです。 GitHubのPullRequest上でSideCIの実行結果を受け取る事ができます。 下記は実際にSideCIから指摘が入った例で、PullRequestに自動的に修正コメントを残してくれます。(最近では、指摘の一部に日本語が導入されました!) その際に、どの種類のLinterから指摘が入ったかと指摘内容を一緒に表示してくれます。 Linterを導入するだけでは開発者がLinterをかけ忘れたり怠ったりした場合、レビュワーが気づいて指摘する手間が発生していましたが、SideCIを使うことでPullRequest上に指摘が自動的に可視化され、直すべきなのに直っていない箇所が一目でわかります。 弊社では、SideCIから来たコメントは全て修正しないとPullRequestをマージしないことになっています。また、緊急対応などの特例で指摘を見逃して欲しい場合はその理由をレビュワーに説明したりコメントをPullRequestに残すルールにしています。 以上がSideCIの説明になります。 SideCI上で自分達が使いたいLinterを選択し、プロジェクトのホームに各Linterの設定ファイルを置くだけでLinterとSideCIを組み合わせて使えるようになります。Linterでコードを自動修正し、修正できてない場合はSideCIを通して指摘してもらうことで規約のチェックを自動化しています。 こうすることで、コーディング規約を守ることを個人的な判断のみに任せず、チーム全体でコーディング規約を守ることができます。 Linterのルールを適度に調節する 上記で、SideCIから来た指摘を全て修正しないとマージしないようにするルールと言いましたが、それを実現するためには各Linterの設定が適切である必要があります。 弊社では、最初チーム内で特定のコーディング規約が存在していなかったため、各Linterのデフォルト設定からスタートして徐々にカスタマイズしていきました。コーディング規約は、チームにとって不要なルールが混ざっていたり、設定がキツすぎるとチームメンバーの負担が大きくなり、不満も募ります。(各Linterのデフォルトの設定はかなりキツめになっています。) なので、実践しながらチームにとって適度なコーディング規約の作成をしていくという方法をとりました。 Linterの設定を適度に調節する際に、 SideCIを使ってLinterの指摘箇所をGithubのコメントを通してシェアする ルールがキツすぎる場合はgithubのissueに議題をあげて議論する 合意が取れる、または1週間以上反論が出ない場合はissueの内容を適用する という方法を繰り返してLinterを調節していきました。 下に実際に上がっていたissueの例です。 このように議論しながら適切な設定を作っていきました。 例1 例2 現在はメンバーの合意が取れている適度なルールになって運用されているため、無理なく無駄なくコーディング規約を守ることに成功しています。 まとめ 以上、APIを長く運用するために仕組み化した下記の3つを紹介しました。 APIのレスポンスを統一するために、 デコレーター と fieldgroup を導入 パラメーターを統一するために全処理共通のValidatorを導入 LinterとSideCIでコーディング規約の徹底 参考になるものがあったら是非試してみてください。 VASILYでは春季インターンの募集を行っています。 APIやバックエンド開発に興味のある方、是非弊社に遊びに来てください!
わーい!コンテナたのしー!🐾 こんにちは。流行りには積極的に乗っていきたい。インフラエンジニアの光野です。 弊社が運営するファッションサイト IQON では、日々200以上の提携ECサイトから100万のオーダーで商品をクロールしています。 新商品の追加・商品の在庫状況・セールの開催など情報は日々変化するため、弊社において「正しくクロールすること」と「速くクロールすること」は肝心カナメの要素です。 本記事では、特に「速くクロールする」という目的で構築した コンテナベースの新クローラーシステム を紹介いたします。 このクローラーシステムは、最終的に クロール時間67%減 、 維持コスト70%減 という成果が得られました。 キーワード: コンテナ, Docker, Apache Mesos, Marathon, AWS Lambda, Amazon EC2 SpotFleet 問題解決手段の検討 -> コンテナ採用 コンテナを採用するまでの経緯を簡単にまとめます。 なお、途中経過を飛ばして最終的な構成から見たい方は こちら からご覧ください。 刷新のモチベーション2016 昨年末の時点で、IQONクローラーは2つの大きな問題を抱えていました。 季節ごとの新商品追加時期など、クロール対象の急激な変化に追従するのが難しい クロール中とそれ以外で負荷の差が激しく、計算資源の無駄が多い 2の影響は弊社で完結しますが、1はユーザ体験の損失につながります *1 。 これを根本的に解決することをモチベーションとして次の3つを満たすべく刷新に着手します。 クロール量の急激な変化に対応してワーカー数を簡単に変更できる 維持コストを抑える(金銭的な削減) 保守コストを抑える(人の手間削減) 改修箇所の選定 幸いにも、どの部分を改修すれば良いかは着手時点で明確でした。 クローラーの実行環境 です。 昨年時点でのアプリケーションとその実行環境は次の通りです。 アプリケーション2016 昨年末時点のIQONのクローラーはver 4.5です。 ver 1.0: PHPで書かれたクローラー 2.0: Rubyで書き換え 3.0: resqueベースの分散処理に移行 4.0: sidekiqベースの分散処理に移行 + クローラー作成ツールの導入 4.5: shoryukenベースの分散処理に移行 ver 4.5は4.0のマイナーアップデート版で、大まかな構成は こちらの資料 にあるものと変わっていません。 クロール処理をステップごとに分解 ステップ毎にそれを担当するプログラム(ワーカー)を準備 ワーカーが並列分散処理を行う という構造です。各ワーカーはそのプロセス毎に完結しており、自分以外のプロセスを意識する必要はありません。 実行環境2016 昨年末時点のIQONクローラー実行環境はとてもシンプルです。 クローラー専用のインスタンスを用意 前述のワーカーをsupervisorでデーモン化 インスタンスのメトリクスを元に、インスタンス毎にワーカーのプロセス数を決定 メッセージ・キューのdequeue速度を元にインスタンスを必要なだけ並べる という構成です。AutoScalingはありません。 コンテナの採用 先の通り、昨年末時点のIQONクローラーはそのアプリケーションに比べて実行環境に柔軟性がありませんでした。 せっかくの分散処理ワーカーもインスタンスに縛られてはその力を発揮しきれません。宝の持ち腐れです。 これを解決するためにIQONクローラーver5.0では実行環境の改善ということを目的にDockerコンテナの導入を行いました。 今までの仕組みの延長線上で、AutoScalingやデプロイの仕組みを整備するという選択肢もありましたが、 かねてより「複数種類のワーカーが多数動く」という状況が「PaaSの上で色々なアプリケーションが動く」という状況に近いと感じていたこともあり、 PaaSを効率化するために生まれた Docker とは相性がいいに違いないという判断でコンテナ化を採用します。 コンテナ採用 -> オーケストレーションツール決定 コンテナの採用を決定後、まず着手したのがオーケストレーションツールの検証です。 オーケストレーションツール Dockerコンテナを本番運用する上で、非常に重要なのがコンテナオーケストレーションツールです(以後、単にオーケストレーションツール)。 Dockerのライフサイクルは1コンテナ単位ですが、本番環境では複数のコンテナを複数のホストにまたがって運用します。 また、弊社ではコンテナイメージの再利用性を高めるために 1コンテナ1プロセス をポリシーにしており、コンテナは複数を論理的に接続して扱うことが前提として求められます *2 。 このようなコンテナ同士を論理的に接続したり、インスタンス群にコンテナを配置したりといったコンテナの組織化(オーケストレーション)を支援してくれるのがオーケストレーションツールです。 オーケストレーションツールの比較検討 今回の刷新では3つのオーケストレーションツール *3 を比較しました。 Kubernetes Marathon Amazon ECS 思い描く実行環境に必要な項目を洗い出し、比較した表が次のものです。 評価項目 Kuberenes Marathon Amazon ECS ELB/ALBとの連携 ◯ x ◯ cron jobのような時間指定でのタスク定義 ◯ x x コンテナのグルーピング ◯ △ (v1.4〜、webui未対応) ◯ マスタのHA構成 ◯ ◯ ◯ (マネージド) Web API ◯ ◯ ◯ Web UI ◯ ◯ ◯ ドキュメントが充実している △ ◯ ◯ クラスタの構築が容易 x ◯ ◯ ホストインスタンスのスケーリングにクラスタが追従する ? ◯ ◯ コンテナデプロイのタイミングでソースコードを配布可能 x ◯ x x: 不可能 / 難しい △: 可能だが制約あり ◯: 可能 このように評価をした結果、作りたい実行環境にとって致命的な x が無く、ツールとしてのアプローチ(後述)がクローラーに最も適している Marathon を採用するに至りました。 補足: ホストインスタンスのスケーリングにクラスタが追従する EC2インスタンスがAutoScalingで増減した際に、それらがクラスタの一部として自動的に認められるかどうかを検討した項目です。 Kubernetesは検証を途中で中断してしまったので ? Marathonはエージェントがマスタノードと通信可能であればクラスタに参加するため ◯ ECSはサービスとしてAutoScalingの機能を提供しているので ◯ としています。 Marathon 今回の刷新で採用したMarathonはそれ単体で動くものではありません。Marathonを使うために、まずApache Mesosを理解する必要があります。 Apache Mesos Apache Mesos は、クラスタを構成するインスタンスのCPUやメモリ、GPUやストレージを1つのリソースプールとして扱う分散システムカーネルです。 Mesosはタスクを受け取ると、リソースに空きがあるインスタンスを見つけてそこで実行します *4 。 利用者からみると、クラスタをあたかも1台の巨大なインスタンスがあるように扱うことが可能です。 Marathonの検証をした時点ではあまり意識していませんでしたが、Mesosの「複数のインスタンスを1つの巨大なインスタンスとして扱う」というアプローチはクローラーと大変相性が良く、積極的にMarathonを採用する後押しにもなっています。 Marathonの役割 Apache Mesosで実行するタスクのライフサイクルは実行から終了コードを受け取るまでです。終了した状態が何であれ、Mesosはそれ以上何もしません。 この動作はWebサービスのようにずっと動き続けて欲しいタスクにとっては不便です。そこでMarathonが登場します。 Marathonは Apache Mesos Framework と呼ばれるもの1つで、Mesosの仕組みの上で タスクに恒常性を与えます 。 アプリケーションのコンテナを4つ、というタスクを投げるとその通りにコンテナを用意し、 仮にコンテナが例外で落ちると要求数を満たすように自動で次のコンテナを用意します。 次のJSONは、実際にコンテナを4つ起動するAPIリクエストの内容です。 メモリ256M、CPUを20% *5 割り当てるコンテナを4つ起動します。 { " parse ": { " id ": " /iqon/crawler/parse ", " container ": { " type ": " DOCKER ", " docker ": { " image ": xxxx } } , " cpus ": 0.2 , " mem ": 256 , " instances ": " 4 " } } fetch機能 Marathonの機能は多岐にわたりますが、ここでは最終的な構成に大きな影響を与えた fetch 機能について紹介いたします。 これのお陰で、比較表で唯一Marathonだけが コンテナデプロイのタイミングでソースコードを配布可能 で ◯ になっています。 fetch はタスクの起動前に、指定したファイルを取得するというMarathonの機能 *6 です。 コンテナをデプロイする際に悩ましいのが、最新のソースコードをどう取得するか、という問題です。 もちろん、コンテナイメージをビルドする際にソースコードを含めることもできますが、 次の問題からコンテナイメージにソースコードを含めることは避けるべきと考えています。 アプリケーションと環境の分離ができない コンテナレジストリに大量のタグが登録される デプロイ時にコンテナイメージのキャッシュが効かない fetch はこの問題を解決します。次のJSONは fetch を利用した場合の例です。 { " parse ": { " id ": " /iqon/crawler/parse ", " container ": { " type ": " DOCKER ", " docker ": { " image ": xxxx } , " volumes ": [ { " containerPath ": " /var/app ", " hostPath ": " ./iqon_crawler ", " mode ": " RW " } ] } , " cpus ": 0.2 , " mem ": 256 , " instances ": " 4 ", " fetch ": [ { " uri ": " https:// xxxx /iqon_crawler.tar.gz ", " executable ": false , " extract ": true , " cache ": false } ] } } fetch と volume の項目は次のように処理されます。 Mesosによる docker run の前に fetch でIQON_crawler.tar.gzが取得/解凍される docker run の --volume で、コンテナに対しマウントされる 検証時に確認した範囲では、同様の機能はMarathonにしかありませんでした。これもまたMarathonの採用を決めた大きな要素です。 オーケストレーションツール決定 -> クラスタ完成 全体構成 オーケストレーションツール選定を経て、IQONクローラーver5.0は最終的に次の形になりました。 ここからは、この実行環境を4つに分けてご紹介します。 アプリケーションのデプロイ まず、アプリケーションのデプロイ部分です。 DockerRegistryには quay.io 、CIには CircleCI を利用しています。 CircleCIがmasterブランチの内容をS3に保存 Marathonへタスクの更新をリクエスト MarathonがMesosへリクエスト MesosがS3から最新のソースコードをfetch Mesosがquay.ioからコンテナイメージをpull Mesosがクラスタのどこかで docker run という流れでデプロイが行われます。 クローラーのリポジトリにはYAMLで書かれたデプロイルールとデプロイ用のスクリプトが用意されており、 CircleCIはそれを使ってMarathonへリクエストを送っています。以下は実際のYAMLファイルから抜粋したものです。 :common : :env : &env :CRAWLER_ENV : production :RACK_ENV : production # ... # private docker reposからpullするための情報 :credentials : &credentials :uri : xxxx :executable : false :extract : true :cache : false :source : &source :uri : https:// xxxx /crawler/app/<%= ENV['CIRCLE_SHA1'] %>.tar.gz :executable : false :extract : true :cache : false :localtime : &localtime :containerPath : /etc/localtime :hostPath : /etc/localtime :mode : RO :network : &network :ipAddress : :networkName : mesos_slave_host_nw # networkがUSERのときはportsを空配列で明示的に宣言しておく必要がある :ports : [] :parse : :id : /iqon/crawler/parse :dependencies : - /iqon/crawler/td-agent :args : - ./scripts/start_shoryuken_worker.rb - --log-level=warn - --worker=parse :env : <<: *env :container : :docker : :image : <%= ENV['DOCKER_APP_IMAGE'] %> :network : USER :parameters : - :key : net-alias :value : parse :volumes : - <<: *localtime - :containerPath : /var/app :hostPath : ./iqon_crawler :mode : RW :type : DOCKER :fetch : - <<: *credentials - <<: *source <<: *network デプロイの4と5で取得しているのは、YAML中に fetch と image で指定されている要素です。 なお、デプロイについては2つの工夫を盛り込みました。 デプロイルールの定義はYAMLで行う YAMLはERBでパースする MarathonへのアクセスはJSONですが、JSONで長い定義を行うのはいささか苦痛です。 そこで、一度YAMLで記述してそれをJSONに変換する形を取ることで、可能な限りDRYに管理できるようにしています。 コメントも書けるため、補足も随時行います。 また、一度ERBでパースすることで環境変数を使えるようにしています。 これでデプロイ毎に変わる部分を気にすること無く fetch で指定することが可能になりました。 コンテナ数のコントロール アプリケーションのデプロイができたので、次はそれをコントロールします。 適当な数のコンテナをデプロイして終わり、であれば良いのですが求めるものは状況に応じてコンテナ数(ワーカー数)が変わる実行環境です。 そこで、Lambda Functionを用いて、CloudWatchとMarathonを繋いでいます。 まず、次のようなYAMLを定義します。 :cloudwatch : :alarm : :iqon-crawler-production-parse-visible-high : :rule : # CloudWatch Alarmに登録する内容 :metric_name : 'ApproximateNumberOfMessagesVisible' :namespace : 'AWS/SQS' :statistic : 'Average' # ... :action : # ruleを受けてLambda Functionがmarathonをどうするか :alarm : :method : 'PATCH' :path : '/v2/apps/' :body : '[{"id": "/iqon/crawler/parse","instances": 32}]' :ok : # ... :insufficient_data_actions : # ... そして、このYAMLをアラーム用のデプロイスクリプトで読み込み、次のように展開します。 YAML中のrule要素を使ってCloudWatchのアラームを登録 YAML中のaction要素を事前に用意しておいたLambda Functionのテンプレートに埋め込む テンプレートをAWS Lambdaに登録 ここでの工夫は関連する処理を1つのファイルで近くに置くということです。 トリガーとアクションを分離しない アプリケーション側でコンテナ配置ルールを管理 トリガーとアクションを並べ、CloudWatchの更新とLambdaへの反映までを一括で行えるようにすることで変更のし忘れを防止しています。 DaemonSetの更新 3つ目は特別なコンテナの管理です。ここが今回もっとも苦労した部分です。 このクローラー実行環境を支える mesos agent node は、Amazon EC2 SpotFleetで宣言されており、 インスタンス自体もまた負荷に応じて増減するようになっています。 SpotFleetが作るインスタンスは起動と同時にユーザスクリプトで systemctl restart mesos-slave が宣言されているため、起動と同時にMesosクラスタの一員となります。 Mesosクラスタ的にはこれで終わりなのですが、この時 AutoScalingに合わせて、td-agentコンテナの数を更新する いう非常に重要な処理を実行しています。 オーケストレーションツール比較表にありますが、Marathonには複数のコンテナを論理的に繋ぐ機能がありません。 Martahon 1.4系で待望のPod機能 *7 が実装されたものの、2017/03/17時点で最新のMarathon 1.4.1では宣言されたPodがWeb UIから見えないという状況のため、導入はまだ時期尚早です。 そして、このPodが無いことで問題になったのがtd-agentコンテナです。 Podであれば必ず同じホスト上にコンテナが配置されますが、Podが無い場合アプリケーションコンテナとtd-agentコンテナは別ホストにデプロイされる可能性があります。 画像右の「偏った状態」になると、右側のインスタンスで動いているコンテナはtd-agentと通信できません。この場合、そのコンテナのログは全て捨てられてしまいます。 これの回避のため、必ず各ホストに1つのtd-agentコンテナが置かれる状況を作る仕組みDaemonSet *8 を動かしています。 DaemonSet実現のために、次の要素を組み合わせます。 Docker Network User Defined Network net-alias Marathon constraints dependencies CloudWatch Events Lambda Function まずDockerのUser Defined Network(以下、UDN)を作成します。 docker network create --subnet=172.20.0.0/16 mesos_slave_host_nw 次にtd-agentの宣言で net-alias を指定します。 # デプロイスクリプト用YAMLのtd-agentコンテナの宣言(抜粋) :td_agent : :id : /iqon/crawler/td-agent :container : :docker : :image : xxxx :network : USER :parameters : - :key : net-alias :value : tdagent これで同一ホストにデプロイされたコンテナからは tdagent という名前解決が可能になります。 UDNを作成するのはこの net-alias がUDN上でしか使えないためです。 次に constraints を宣言し、デプロイに制約を加えます。 :td_agent : :id : /iqon/crawler/td-agent :container : :docker : :image : xxxx :network : USER :parameters : - :key : net-alias :value : tdagent :constraints : - - hostname - UNIQUE Maratahon(Mesos)はタスクを余裕のあるインスタンスで実行しますが、実行先インスタンスに幾つかの制約を持たせる可能です。 hostname , UNIQUE は、そのタスクを各ホストで高々1つまでしか起動できなくするという制約です。 次にCloudWatch Alarmを登録する仕組みを応用して、CloudWatch Eventを登録します。 :autoscaling : :daemonize-container : :rule : :schedule_expression : "cron(*/2 * * * ? *)" :action : :method : 'PATCH' :path : '/v2/apps/' :targets : [ 'iqon/crawler/td-agent' ] 2分置きにイベントを発生させ、対応するLambda Functionを発火させます。 Lambda FunctionはMesosクラスタを構成するインスタンス数を取得し、actionに従ってMarathonにコンテナ数変更のリクエストを投げます。 [ { " id ": " iqon/crawler/td-agent ", " instances ": " Mesosクラスタのインスタンス数 " } ] 最後に、アプリケーションコンテナへ dependencies と env でtd-agentを指定してやれば完了です。 :parse : :id : /iqon/crawler/parse :dependencies : - /iqon/crawler/td-agent :env : :TD_AGENT_HOST : tdagent # アプリケーション側で環境変数を読む dependencies が宣言されると、そのタスクは dependencies で宣言されたタスクが実行されるまで自身の実行を待ちます。 ここまでの流れを整理します。 td-agentコンテナはUDNのnet-aliesを利用し、tdagentという名前で名前解決が可能になる td-agentコンテナは constraints に従って1ホスト1コンテナが保証されている アプリケーションコンテナはtd-agentコンテナが起動するまで起動を待つ 起動後はtdagentという名前でtd-agentコンテナを探す Lambda Functionによる変更で、ホスト数分だけtd-agentコンテナが用意されるため、 constraints と合わせて各ホストには必ずtd-agentが存在する ここの説明ではtd-agentに限っていますが、必要に応じて別のタスクも管理可能なようになっています。 コンテナイメージの更新 最後はコンテナイメージの世代更新についてです。 IQONクローラーのリポジトリでは、masterブランチ以外にdockerbuildという特別なブランチを用意しています。 circle.ymlからdockerbuildに関する部分を抜粋したものが次のYAMLです。 machine : environment : DOCKER_APP_IMAGE : xxxx CONTAINER_NAME : yyyy services : - docker dependencies : pre : - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io - docker pull ${DOCKER_APP_IMAGE}; true override : - if [ $ { CIRCLE_BRANCH } = 'dockerbuild' ] ; then docker build -t ${CONTAINER_NAME}:${CIRCLE_SHA1} .; else true ; fi test : # ... deployment : registry : branch : "dockerbuild" commands : - docker push ${CONTAINER_NAME}:${CIRCLE_SHA1} dockerbuildブランチ以外では、 DOCKER_APP_IMAGE で宣言されたコンテナイメージをpullして利用します。 一方、dockerbuildブランチではpullされたコンテナイメージだけでなく、リポジトリのDockerfileを使いコンテナイメージを作成します。 テストを通過すればコンテナイメージがquay.ioにpushされるため、タイミングをみて DOCKER_APP_IMAGE を更新しmasterへマージします。 知らぬ間にコンテナが入れ替わって不具合発生というリスクを最小限に抑えるため、ここでも全てをアプリケーションのリポジトリにまとめる形式を採用しています。 刷新の効果 さて、少なくない工数をかけて行ったIQONクローラーver5.0(コンテナ化)ですが、その効果は目覚ましいものがありました。 効果 クロール時間 67%減 維持コスト 70%減 保守コスト Web UIで変更。設定が固まったらYAMLに反映 柔軟にワーカー数を変更できるようになったことで、 クロール開始直後はHTMLをダウンロードするワーカーを増やす クロールの後半ではHTMLをパースするワーカーを増やす ということが可能になり、その結果クロール時間は刷新前の3分の1になりました(67%減)。 また、インスタンスの維持コストも大幅に削減できました。瞬間的にはこれまでの3倍近い計算資源を用意して尚、トータルでは70%の維持コスト削減となっています。 これはSpotFleetを採用した影響が大きいですが、コンテナのメトリクスとしてワーカーが必要とするCPUとメモリが可視化されたため、集約度を上げられたことも要因の1つです。 3つめの保守コストについては言うまでもありません。 インスタンス起動・Chef実行・デプロイ・ワーカー起動という作業がWeb UI1つで誰でも行えるようになりました。 クラスタの運用はありますが、その手間を差し引いても極めて大きな保守コスト削減ができています。 まとめ この記事では、Docker / Apache Mesos / Marathonを使った新クローラーシステムの紹介をいたしました。 長々と読んでいただき誠にありがとうございます。少しでも得るものがありましたら何よりです。 今回のクローラー刷新では、アプリケーションを柔軟に実行できる環境の構築を目指して取り組み、 クロール実行時間の短縮と維持コスト、保守コストの大幅な削減とおよそ最高の結果を得ることができました。 またこの記事では、話の都合上クラスタ構築中の試行錯誤やコンテナ移行後の開発環境に関しての話題を丸々省略しております。 そちらは別記事としてまとめるつもりですが、もし興味を持っていただけるようであれば、はてブやSNSでリアクションをいただければ幸いです。まとめの励みになります。 なお、動き出したばかりのシステムということもあり、インスタンス数やワーカー数もまだまだ最適化できる余地が残っています。 今後はより安定して無駄がないものになるよう、運用しつつ工夫を続けるつもりです。 VASILYでは、そんな現状に満足せず技術的チャレンジを好むエンジニアを募集しております。興味を持っていただけた方は是非下のリンクからご応募下さい。 *1 : 実際に、情報の反映が遅れたことが原因で「IQONとECサイトで掲載価格が違う」というお問い合わせをいただくことがありました *2 : クローラーであればアプリケーション+td-agentといった組み合わせ *3 : その他のオーケストレーションツールとしてDocker Swarm, HashiCorp Nomadがありますが、ドキュメントを読んでみてしっくりこなかったため検討対象から外しています *4 : 当然、クラスタに所属するインスタンスはタスクの実行環境を持っておく必要があります *5 : cpusの設定はDockerの cpu-sharesオプション に反映されます。そのためインスタンスに余裕さえあれば必要に応じて20%以上が割り当てられることもあります *6 : 正確にはApache Mesosが提供する fetcher という機能をMarathonが呼んでいます *7 : KubernetesにはPodというコンテナのグルーピング機能がありPodに所属するは同一ホスト上にあることが保証されます。MarathonのPodも同様です。 *8 : Kubernetesには、各ホストで必ず動いていて欲しいコンテナを宣言するDaemonSetという仕組みがあり、それに倣いました
iOSエンジニアの庄司 ( @WorldDownTown ) です。 最近、業務で新しいiOSアプリを立て続けにいくつか開発する機会に恵まれました。 そんな中、いくつもアプリを使っていると、どのアプリでもよく使う処理があぶり出されてきます。 そういう処理はSwiftのExtensionとして別ファイルに書き出し、他のアプリへも切り出しやすいように個別のFrameworkにして管理しています。 Frameworkの管理については過去の こちらの記事 を参考にしてみてください。 今記事では、最近の開発でよく使ったExtension集をご紹介します。 Swift標準ライブラリ Date private let formatter : DateFormatter = { let formatter : DateFormatter = DateFormatter() formatter.timeZone = NSTimeZone.system formatter.locale = Locale(identifier : "en_US_POSIX" ) formatter.calendar = Calendar(identifier : .gregorian) return formatter }() public extension Date { // Date→String func string (format : String = "yyyy-MM-dd'T'HH:mm:ssZ" ) -> String { formatter.dateFormat = format return formatter.string(from : self ) } // String → Date init ?(dateString : String , dateFormat : String = "yyyy-MM-dd'T'HH:mm:ssZ" ) { formatter.dateFormat = dateFormat guard let date = formatter.date(from : dateString ) else { return nil } self = date } } Date().string(format : "yyyy/MM/dd" ) // 2017/02/26 Date(dateString : "2016-02-26T10:17:30Z" ) // Date 同じモデルクラスを使って日付を表示する場合でも、リストページでは日付だけ、詳細ページでは時間も表示したいというときに便利です。 またイニシャライザの方は、ユーザーが入力した日付文字列から Date インスタンスを作ることができます。 Dictionary // Dictionary同士を`+`演算子でマージできるようにする public func +< K, V > (lhs : [K: V] , rhs : [K: V] ) -> [K: V] { var lhs = lhs for (key, value) in rhs { lhs[key] = value } return lhs } [ "key1" : 0 ] + [ "key1" : 1 , "key2" : 2 ] // ["key2": 2, "key1": 1] APIリクエスト時の動的パラメータと固定のパラメータのDictionaryをマージするときなどに使います。 Int private let formatter : NumberFormatter = NumberFormatter() public extension Int { private func formattedString (style : NumberFormatter.Style , localeIdentifier : String ) -> String { formatter.numberStyle = style formatter.locale = Locale(identifier : localeIdentifier ) return formatter.string(from : self as NSNumber) ?? "" } // カンマ区切りString var formattedJPString : String { return formattedString(style : .decimal, localeIdentifier : "ja_JP" ) } // 日本円表記のString var JPYString : String { return formattedString(style : .currency, localeIdentifier : "ja_JP" ) } // USドル表記のString var USDString : String { return formattedString(style : .currency, localeIdentifier : "en_US" ) } } let million : Int = 1_000_000 million.formattedJPString // 1,000,000 million.JPYString // ¥1,000,000 million.USDString // $1,000,000.00 どのアプリも価格を表示するところは多数あると思いますが、計算型プロパティ一つで面倒なカンマ区切り処理を書けるので、コードがかなりスッキリします。 UIKit UIColor public extension UIColor { // RGBのイニシャライザ public convenience init (rgb : UInt , alpha : CGFloat = 1.0 ) { let red : CGFloat = CGFloat((rgb & 0xff0000 ) >> 16 ) / 255.0 let green : CGFloat = CGFloat((rgb & 0x00ff00 ) >> 8 ) / 255.0 let blue : CGFloat = CGFloat(rgb & 0x0000ff ) / 255.0 self . init (red : red , green : green , blue : blue , alpha : alpha ) } public struct iq { // プロジェクトに合わせた名前で良い public static let pink : UIColor = UIColor(rgb : 0xfa4664 ) public static let textBlack : UIColor = UIColor(rgb : 0x333333 ) } } UIColor.iq.pink // #fa4664 UIColor.iq.textBlack // #333333 一つのプロジェクトで使う色数は、数え切れる程度になることが多いので、 UIColor のExtensionに名前を付けて管理します。 Extensionの中でstructを定義して、その中に色をまとめることで、 UIColor の本来の名前空間を汚してしまうことがなくなりますし、開発者が拡張しているというのがコードの読み手にも伝わりやすくなります。 UIView public extension UIView { // 子Viewを親Viewのサイズいっぱいに表示するための制約を設定する func addConstraints ( for childView : UIView , insets : UIEdgeInsets = .zero) { childView.translatesAutoresizingMaskIntoConstraints = false topAnchor.constraint(equalTo : childView.topAnchor , constant : insets.top ).isActive = true bottomAnchor.constraint(equalTo : childView.bottomAnchor , constant : insets.bottom ).isActive = true leadingAnchor.constraint(equalTo : childView.leadingAnchor , constant : insets.left ).isActive = true trailingAnchor.constraint(equalTo : childView.trailingAnchor , constant : insets.right ).isActive = true } } view.addSubview(childView) view.addConstraints( for : childView ) let insets : UIEdgeInsets = UIEdgeInsets(top : 10.0 , left : 10.0 , bottom : 10.0 , right : 10.0 ) view.addConstraints( for : childView , insets : insets ) Interface Builderではなく、コードでviewを addSubview するとき、親Viewと同じサイズにしたいときに使います。 デフォルト引数は省略可能ですが、 UIEdgeInsets でマージンを指定することもできます。 UIViewController public extension UIViewController { // ViewControllerのファクトリーメソッド static func create () -> Self { let name : String = " \(type(of: self) )" .components(separatedBy : "." ).first ! return instantiate(storyboardName : name ) } private static func instantiate <T> (storyboardName : String ) -> T { let storyboard : UIStoryboard = UIStoryboard(name : storyboardName , bundle : nil ) let vc : UIViewController ? = storyboard.instantiateInitialViewController() return vc as ! T } } let vc = SomeViewController.create() VASILYのiOS開発では、 1ViewController / 1Storyboard でViewControllerを管理し、ViewControllerのクラス名とStoryboardのファイル名を揃えるルールで運用しています。 そのため、文字列を使わずにViewControllerを初期化することができます。 サードパーティライブラリ SVProgressHUD import SVProgressHUD public extension SVProgressHUD { public struct iq { // プロジェクトに合わせた名前で良い // プロジェクト固有の初期設定 public static func setup () { SVProgressHUD.setDefaultStyle(.custom) SVProgressHUD.setFont(UIFont.boldSystemFont(ofSize : 14.0 )) SVProgressHUD.setForegroundColor(UIColor.iq.pink) SVProgressHUD.setBackgroundColor(UIColor.white.withAlphaComponent( 0.9 )) SVProgressHUD.setMinimumDismissTimeInterval( 2.0 ) } public static func show (maskType : SVProgressHUDMaskType = .none) { SVProgressHUD.setDefaultMaskType(maskType) SVProgressHUD.show() } } } サードパーティライブラリは、アプリ全体で設定を有効にするために、AppDelegateに処理を書くことがよくあります。 複数のライブラリを使っていると、 application(_:didFinishLaunchingWithOptions:) -> Bool が 各ライブラリの初期設定処理で膨れ上がってしまいます。 上記のExtensionは、複数行の処理をExtensionにまとめることによってこの問題を回避できるようになります。 ここでも、サードパーティライブラリの名前空間を汚さないように処理をstructの中に分けて書いています。 // AppDelegate func application (_ application : UIApplication , didFinishLaunchingWithOptions options : [Hashale: Any] ) -> Bool { SVProgressHUD.iq.setup() ... return true } また、現在のSVProgressHUDはMaskTypeがグローバルに設定されてしまうため、MaskTypeを変更したいときは、 setDefaultMaskType(_:) を実行してから表示する必要があります。 このExtensionでは2行必要な処理を一つのメソッドでMaskTypeを引数に取れるように変更しています。 小さな変更ですが、SVProgressHUDはどのアプリでも頻繁に登場するため、冗長なコードを減らすことができます。 // 従来 SVProgressHUD.setDefaultMaskType(.clear) SVProgressHUD.show() SVProgressHUD.setDefaultMaskType(.none) SVProgressHUD.show() // Extension SVProgressHUD.iq.show(maskType : .clear) SVProgressHUD.iq.show() // デフォルト値:.none まとめ iOSアプリ開発でよくある処理のExtensionの数々を紹介しました。 これらの処理が一つのFrameworkにまとまっていると、新しいアプリを作る時にも共通処理を持ってくることができます。 新規開発時に試してみてください。 現在VASILYでは、IQON以外にも新規でいくつかiOS/Androidアプリを開発しています。 ゼロからアプリを一緒に開発してくれるiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。
こんにちは。 iOSエンジニアの遠藤です。 最近ユーザー詳細ページのリニューアルをすることになり、UIStackViewで実装しました。 UIStackViewを使ってとてもシンプルに実装できたので、UIStackViewで詳細ページを実装するメリットと実装について紹介します。 はじめに このような表示コンテンツの多い詳細ページを実装する際に、みなさんは何を使用していますか? UIStackViewはiOS 9から追加されたクラスですが、 まだUITableViewやUICollectionViewで詳細ページを実装されている人も多いのではないでしょうか? IQONも詳細ページをUITableViewやUICollectionViewを使って実装してきました。 しかし、UITableView、UICollectionViewは同じモジュールの繰り返しを表示するのには適していますが、詳細ページなどの違うモジュールを表示するのには、ビジネスロジックのコードなどでDataSourceがとても複雑になってしまいました。 UIStackViewを使うことで、複雑になりがちな詳細ページがとてもシンプルに実装することができました。 詳細ページを実装する際に、少しでも参考になれば幸いです。 UIStakcViewを使って詳細ページを実装する際のメリット・デメリット UIStackViewの基本的な使い方は下記の記事が分かりやすくておすすめです。 遅ればせながら UIStackView 入門 - Qiita メリット データの出し分けが簡単 モジュールのサイズ計算をしなくていい モジュール間のスペースを気にしなくていい UITableViewやUICollectionViewを使用して実装していたときに、一番大変なのはDataSourceの実装ではないでしょうか? class DetailViewController : UICollectionViewController { private func cellIdentifier (_ indexPath : IndexPath ) -> String { switch indexPath.section { case 0 : switch indexPath.item { case 0 : return UserDetailHeaderCell.cellIdentifier() case 1 : ・・・ } } override func numberOfSections ( in collectionView : UICollectionView ) -> Int { return (viewModel.user != nil ) ? 10 : 0 } override func collectionView (_ collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int { guard let section = UserDetailSection(rawValue : section ) else { return 0 } switch section { case .userName : return 2 case .userDescription : return viewModel.shouldShowDescription ? 1 : 0 case .userSets : return viewModel.sets.count ・・・ } } override func collectionView (_ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell { let cellIdentifier = self .cellIdentifier(indexPath) let cell = collectionView.dequeueReusableCell(withReuseIdentifier : cellIdentifier , for : indexPath ) return cell } func collectionView (_ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , sizeForItemAt indexPath : IndexPath ) -> CGSize { if let cachedCellSize = cachedCellSize[indexPath.item] { return cachedCellSize } // cellのサイズ計算 let size : CGFloat = ・・・ let key = " \(indexPath.section) - \(indexPath.item) " cachedCellSize[key] = size return size } } 上記のコードのように、データがある場合とない場合とで表示するモジュール数を変えたりモジュールごとにCellを出し分けたりとコード量が多くなりがちです。 また、Cellのサイズ計算も楽ではありません。 レイアウトを元にCellのサイズを計算したり、スクロールパフォーマンスを悪くしないためにCellのサイズをキャッシュしたりするので複雑になってしまいます。 しかし、UIStackViewを使えばDataSourceもCellのサイズ計算も必要ありません。 UIStackViewはサブビューをhiddenにすることで、非表示にできるのでデータの有り無しの制御が簡単です。 また、UIStackViewはサブビューの intrinsicContentSize をもとにしたサイズで描画されるのでAutoLayoutをきちんと設定すれば、サイズ計算のためのコードを書く必要がありません。 デメリット iOS 9以降でしか使用できない 各モジュール間ごとのスペースを自由に設定することはできない iOS 9以降の機能なので、iOS 8をサポートしたい場合には、バージョンで出し分けの処理を書く必要があることが一番のデメリットだと思います。 また、UIStackViewではモジュール間のスペースを自由に設定することはできないため、デザインでモジュール間のスペースが異なる場合は実装が大変になると思います。 また、デザイナーもモジュールの表示、非表示すべてのパターンを網羅することは大変です。 そこはデザイナーに相談してすべてのモジュール間を等間隔にしてもらうことをおすすめします。 実装について 今回、詳細ページを実装する上で考慮したのはモジュールの出し分けをViewController側に書かないようにすることです。 ViewControllerにモジュールの出し分けを書かないようにすることで、ViewControllerの肥大化を防ぐことができます。また、ViewControllerはViewModelから受け取ったModelをViewに渡すだけにすることでコードがシンプルになります。 実装は以下のとおりです。 // ViewModel.swift class ViewModel { var modelA : Model ? var modelB : Model ? func request () { // APIリクエストして返ってきたレスポンスをもとに各Modelオブジェクトを更新 } } // ViewController.swift class ViewController : UIViewController { private var viewModel : ViewModel = ViewModel() @IBOutlet private weak var viewA : CustomView ! @IBOutlet private weak var viewB : CustomView ! override func viewDidLoad () { super .viewDidLoad() viewModel.request { [weak self ] in self ?.setupLayout() } } private func setupLayout () { viewA.model = viewModel.modelA viewB.model = viewModel.modelB } } // ContentView.swift class CustomView : UIView { var model : Model ? { didSet { setupLayout() } } private func setupLayout () { guard let model = model else { return } isHidden = (model == nil ) } } UITableViewやUICollectionViewで実装するよりもはるかにシンプルになったと思います。 この実装方法だと、表示するモジュールが多くなった場合でもViewControllerが肥大化しなくて済みます。 また、表示しているモジュールの並び替えがとても簡単で、UIStackViewのサブビューの並びを変えるだけで良いのです! まとめ UIStackViewを使った詳細ページの実装について紹介しました。 表示要素が多い詳細ページはUITableViewやUICollectionViewを使用して実装するとViewControllerが肥大化してしまいがちでです。 UIStackViewを使用することでViewControllerの実装が複雑にならず肥大化せずに実装でき、メンテナンスもしやすくなるので詳細ページをUIStackViewで実装するのはおすすめです! ぜひ、詳細ページを実装する際に試してみてください。 さいごに VASILYではデザインの実装にこだわるエンジニアを募集しています! 少しでもご興味のあるかたは以下のリンク先をご確認ください。
Androidエンジニアの @nissiy です。Androidが発表されてからもうすぐ10年になろうとしています。長いですね。 実は Android版IQON 、今年の4月でリリースしてから丸5年を迎えます。ここまで長くサービスを続けられて、かつ3年連続でベストアプリをいただけたのは、使ってくれているユーザーの方々のおかげであると日々感謝しています。 この5年で様々な追加機能の開発を行ってきました。新機能を1つ追加する度に、古い機能を1つ削除することを徹底して開発を進めてきたものの、長く開発を続けているのでそれなりに巨大なアプリになっています。 今回はAndroid版IQONを長く開発し続けるためにチーム内で徹底しているルールをいくつか紹介したいと思います。 当たり前な話ばかりですが、大きくOSのアップデートを繰り返すAndroidのアプリ開発に取っては大事な話ばかりですので、少しでも参考にしていただけると嬉しいです。 1. 使用しなくなったクラスやリソースファイルはすぐに消す 1つのアプリを長く開発していると節目節目に大きくリファクタリングが行われると思います。そうすると使用しなくなったクラスやリソースファイルが現れますが、そのようなファイルはすぐに消すことを徹底しています。 一度不要になったものを再度使用することなんてほとんどありませんし、仮に必要になったらコミットログから引っ張り出せばいいだけなので容赦なく削除しましょう。 アプリサイズ削減にも直接効くので消し忘れがないように、Android Studioの Inspect Code を実行して探し出すのがポイントです。 2. Googleが提供しているAPIや、Support Libraryを標準に沿って使い倒す Googleが提供しているAPIや、Support Libraryを標準に沿って使い倒すことがAndroidの開発においてはとても重要になってきます。 標準に沿って使うことで、ガイドラインに変更が入ったりした際に移行がとてもスムーズにできるようになりますし、正しいMaterial Design(Androidのデザインガイドライン)の実装に近づけることができます。そのため、標準から大きく外れた使い方はなるべく控えた方がいいと思います。 仕様上、デザイン上、どうしても標準から外れた実装をしなければならない状況になりましたら、ある程度の覚悟を決めてカスタマイズに望みましょう。 3. サードパーティ製のUI系Libraryは慎重に選定する Design Support Libraryが充実してきたおかげで最近は使うことが少なくなったサードパーティ製のUI系Libraryですが、もしも使用しないといけない場合が来たら慎重に選定することをオススメします。 UI系Libraryはガイドライン変更の影響を受けやすいため、標準から大きく外れた実装をしていると移行が大変になるケースが多いです。IQONも過去に何度か苦しめられました... 使うことになった際には、開発が活発であるか、長い間使われていて枯れているか、使用するメリットが大きいか、他社での導入実績があるか、など多くの面を調査して選定することをオススメします。 4. コンストラクタやpublicメソッドの引数にはAnnotationを付けるようにする コンストラクタやpublicメソッドの引数にはAnnotationを付けるように徹底しています。 IQONで良く使われているAnnotationは以下になります。 @NonNull @Nullable @DrawableRes @LayoutRes @StringRes @AnimRes @MenuRes Annotationを付けることで誤ったものを渡したときに警告してくれるようになるので開発中に非常に助かります。アプリの規模が大きく、長く開発を続けているアプリであれば恩恵は大きいと思います。 @NonNull と @Nullable に関しては、チームでNullableになるような実装はしないと堅く決めるのであれば必要ないと思いますが、Nullableになる可能性があるのであれば付けたほうが安全です。 またKotlinで書く場合であれば、Nullableの場合は型に ? を付けるだけでスマートにできるため @NonNull と @Nullable は必要ありません。 5. テキスト、色、余白などの情報はResourceファイルにまとめて必要最低限に保つ テキスト、色、余白などの情報は、レイアウトファイルやJava(Kotlin)側に直接書かず、Resourceファイルに定義して使うように徹底しています。Resourceファイルに定義したものを使うようにすることで、アプリ全体で長期的に統一感を保つ効果があります。 ただし余白に関しては、画面特有の指定がある場合には直接書く場合もあります。 また、定義するだけではダメで 必要最低限に保つ ことが非常に重要です。長く開発を続けていると自然とダブリが発生したりしますが、それを放置せずに常に見直すようにして必要最低限にすることで、定義した情報を使用する際の混乱を防ぐことができます。 6. チームでコードの体裁を揃える Android Studioの Reformat Code や Optimize Imports を実行して、チームでコードの体裁を揃えることも長く開発し続けるには重要だと考えて行っています。 長く開発を続けているとどうしても人の入れ替わりが発生しますが、共通のルールがないとコードの細かい部分で統一感がなくなってきて少しずつ気になりはじめます(各チームメンバーの性格もあると思いますが)。 今では全員Android Studioを使って開発をしており、体裁を整えることに関してはそこまで苦ではないためルールを設け揃えるように徹底しています。 細かい話ではありますが、いろんな性格のメンバーがいる開発現場では大切なルールです。 7. レイアウトファイルはできる限りシンプルにする 以下の2点に関してはレイアウトファイルをコードレビューする際に注意して見るようにしています。 まとめることで削除できるViewは削除する 不要なパラメータは付けないようにする まとめることでViewが削除できたり、階層が減らせたりする場合はコードレビューで積極的に指摘しています。最近の端末であればそこまで大きくパフォーマンスに影響はでませんが、古い端末であれば顕著に影響が出る場合があるのでムダがあればドンドン減らしましょう。 特に仕様変更やリファクタリングで修正が入った場合には見落としがちなので注意して見るようにしています。 仕様変更やリファクタリングによって使うViewが変わった際には元のViewで使っていたパラメータが残っている場合が多いので注意してみるようにしています。RelativeLayoutにorientationのパラメータが付いているケースはとてもよく見かけます。 また、あってもなくてもUIに影響がでないようなパラメータも付けないようにしています。本当に必要なときにだけ必要なパラメータを付けることでXMLがスッキリし改修する際に手が入れやすくなります。 ただし、Android Studioが警告してくれるものに関しては準拠するようにしています。 8. APIをシンプルな形で使う APIの仕様を把握しシンプルな形でAPIを使うことは、アプリを長く開発し続けるのに重要だと考えています。Android DevelopersのDocumentを読んだり、実際にコードを読んだりしてみると、メソッドがオーバーロードされていてよりシンプルな形で使えるようになっていることがあります。 例えば ActionBar の setTitle のメソッドは、引数が CharSequence のものと、 @StringResのint のもの、2つが提供されています。 それにも関わらず getSupportActionBar().setTitle(getString(resId)) と書いてしまっているコードをよく見かけるので、APIの仕様をしっかりと把握してよりシンプルになるようにしています。 9. 新しい仕組み・古い仕組みを併用している場合、古い仕組みにはdeprecatedを付ける アプリを長く開発していると場所によって古い仕組みが残ってしまっているケースが多いと思います。すぐにリファクタリングをして古い仕組みを撤廃できれば全く問題ないのですが、依存している箇所が多いと改修に多大な時間がかかってしまうため併用している場合も多いのではないかと思います。 IQONの場合もAPIクライアントがその状態に当たります。ほとんどのAPIリクエストに関してはRetrofitを使うように書き換えたのですが、一部Apache HTTP Clientをベースに作られたAPIクライアントが残っています。そのまま併用していると間違って新規で使われてしまう恐れがあるため、古いAPIクライアントには deprecated を付けて古い仕組みである、リファクタリングの対象である、ということを明確にしています。 最後に Androidアプリを長く開発し続けるために気をつけている9個のルールを紹介しましたが、Android特有のルールがありつつも結局のところは、 ムダなコードは書かない シンプルに作る 他のメンバー(未来の自分)のことを考えて作る これに尽きると思います。 この10年でAndroidは世界で一番使われているモバイルOSになりました。これから長く開発されていくアプリが増えてくると思いますが、今回紹介したことがあまりできていない場合は見直してみることをオススメします。 最後の最後に、現在VASILYではIQONだけではなく、新規でいくつかAndroidアプリを開発しています。 100%Kotlinで書いているプロジェクトもあったりしますので興味がある方は一度遊びに来てください。
こんにちは、バックエンドエンジニアの塩崎です。 今まではiQONの全文検索用のインデックスには形態素解析だけを用いていましたが、先日Ngramも併用することで検索を改善しました。 その結果、検索結果のヒット数が向上し、なおかつ検索ノイズの増加を軽微なものに抑えることができました。 この記事では、Ngramを併用することのメリット、およびそれをApache Solrで利用する方法について紹介します。 欲しい情報が見つからないとは そもそも、「検索したけど欲しい情報が見つからない状態」とはどのような状態でしょうか? ここではその状態を以下の2つの状態に分解して考えてみます。 欲しい情報の数が少ない 1つ目の状態は「欲しい情報が検索結果中に少ない」状態です。 例えば、旅行情報サイトで「東京」と検索した時にDBの中には数千件のデータがあるのに検索結果数がわずか数件しかないような状態です。 欲しくない情報の数が多い 2つ目の状態は「検索結果中の欲しくない情報の数が多い」状態です。 「東京」と検索した時に、東京の情報以外に京都、大阪などの他の地域の情報が検索結果に含まれるような状態です。 実際は2つの状態が同時に起こっている 実際の欲しいものが見つからない状態は上記の2つの状態が同時に発生していることが多いです。 つまり、ユーザーの欲しい情報全てを検索結果として返しておらず、また、ユーザーの欲しくない情報も検索結果として返している状態です。 ベン図で表すと以下のようになります。 DBに格納されている全情報を、検索結果として返したか否か、欲しかった情報か否かという2軸で分類しています。 そして、これ以降の説明のためにそれぞれの集合に名前をつけます。 検索結果として返していて、なおかつユーザーの求めていた情報を「正しい結果」、検索結果として返しているのに、ユーザーが求めていない情報を「検索ノイズ」、ユーザーが求めているのに、検索結果に含ませない情報を「検索漏れ」とします。 ちなみに、情報検索の分野では、それぞれを「TruePositive」、「FlasePositive」、「FalseNegative」と呼びます。 この図で検索漏れが多い状態が上で説明した状態の1つ目の状態、検索ノイズが多い状態が2つ目の状態に相当します。検索結果の改善とは検索ノイズと検索漏れの数を減らし、検索結果と欲しい情報を一致させることに他なりません。 しかし、これら2つを減らすことはトレードオフの関係にあります。どちらか1つを改善することによってもう1つが悪化してしまうことが多くあります。 検索インデックスの種類とその利点・欠点 全文検索エンジンの中で行なわれているインデックス処理と、その前段階に行なわれる単語分解について説明します。 全文検索エンジンは検索処理を高速化するために、転置インデックスというインデックスを生成します。 転置インデックスは文章中の単語をキー、その単語が出現するドキュメントの配列をバリューとする連想配列です。 このあたりについては先日公開したTECH BLOGの記事の前半部分で詳しく説明しているので、よろしければご覧ください。 Solr 6でneologdが組み込まれたkuromojiを使う方法 転置インデックスの生成のためには、ドキュメントを単語に分解する必要があります。 日本語のような単語と単語の間が空白で区切られていない言語を単語分解するために、Ngramと形態素解析という2つの手法があります。 それぞれの特徴について説明します。 Ngram Ngramは文章を機械的にN文字づつに区切って単語分解する方法です。 何文字ごとに区切るかでNの部分が変わり、特にN=2の場合はbigram、N=3の場合はtrigramとも言います。 これ以降では説明のためにN=2(bigram)を利用します。 bigramではドキュメントを2文字ごとに区切って、その結果を単語としてみなします。 例えば、「東京都美術館」というドキュメントは以下の5つの単語に分解されます。 「東京」「京都」「都美」「美術」「術館」 そして、検索を行うときには、検索クエリを同様に単語分割して、それらのANDで結合します。 例えば、「美術館」という検索クエリに対しては「美術 AND 術館」という条件で検索を行います。 このようにすることによってN文字以上の任意の部分文字列について検索漏れがなくなることが保証されるのがNgramの利点です。 一方で「京都」という検索クエリに対してもこの「東京都美術館」がヒットしてしまうことがNgramの欠点です。 利点:部分一致検索ができることが保証されている 欠点:検索ノイズが多い 形態素解析 形態素解析による単語分割は、事前に用意した辞書を用いて、文法的に意味のある単位で単語分割を行います。 そのため、Ngramを使用した時の問題は起こりづらいことが利点です。 一方で、辞書を事前に用意する必要があり、辞書の性能が低いと検索性能が悪化してしまいます。 例えば「外国人参政権」を「外国」「人参」「政権」と分解してしまうと、そのドキュメントは「参政権」という検索クエリにヒットしなくなってしまいます。 また、SolrやElasticSearchに標準で搭載されている形態素解析器であるkuromojiはIPA辞書を利用していますが、この辞書は特定領域の固有名詞にそこまで強いわけではないという弱点があります。 そのため、本来は1単語になるべき固有名詞が複数の単語に分解されてしまうことがままあります。 例:「ロクシタン」 → 「ロク」「シタン」 一方で、固有名詞を一単語とすることにも別の問題があります。 例えば、「関西国際空港」という固有名詞を一単語としてインデックスすると、「国際空港」や「空港」といった検索クエリで「関西国際空港」が含まれるドキュメントがヒットしなくなってしまいます。 これを解消するために、「関西国際空港」という単語をさらに分割して、「関西」「国際」「空港」の3単語と合わせて合計で4単語をインデックスする機能がkuromojiに備わっています。 しかし、このような挙動も辞書や形態素解析器依存であり、必ずしも部分一致検索ができる保証はありません。 利点:検索ノイズが少ない 欠点:辞書の性能によっては検索漏れが多い 形態素解析とNgramの併用 Ngram、形態素解析の利点欠点を検索ノイズの数、検索漏れの数という観点で以下の表にまとめます。 検索ノイズ 検索漏れ Ngram 多い 少ない 形態素解析 少ない 多い この図からも検索ノイズを減らすことと検索漏れを減らすことがトレードオフの関係にあることがわかります。 しかし、これら2つを併用することによって、それぞれを単独で使用する場合に比べて、検索漏れの数を減らし、検索ノイズの数を実質的に減らすことが可能です。 併用するための具体的な手法は以下のようなものになります。 インデックス生成処理 ドキュメントに対してNgramによるインデックスを生成する 形態素解析によるインデックスも生成する 検索処理 Ngramと形態素解析によるインデックスに対して同時に検索を行う 形態素解析によるインデックスの結果が先頭になりやすいように重みを付けて検索結果をマージする Icons made by Madebyoliver from www.flaticon.com is licensed by CC 3.0 BY 2つの検索結果をマージすることによって、検索漏れを減らします。 しかし、ただ単にマージしてしまうと、検索ノイズが増えてしまいます。 形態素解析の結果を先頭になりやすいようにマージすることが、この併用処理のキモです。 ユーザーが検索結果を見るときにはその先頭から見るため、先頭に求める情報があった場合はそこで満足してそれ以降の結果を見るのをストップすることがあります。 そのようなケースにおいては実質的に検索ノイズは増えていないと考えることができます。 形態素解析とNgramを併用するクエリをSolrで作る方法 具体的なSolrの設定ファイルの書き方、クエリの書き方について説明します。 これらの動作はSolr 6.2.1で確認していますが、特定のバージョンに依存した書き方はしていないため、他のバージョンのSolrでも動くかと思います。 インデックス生成方法 まずはインデックスの生成部分です。 以下の3つの情報をmanaged-schemaに書き込みます。 フィールドタイプ定義 Ngramでインデックスの生成を行うためのfieldTypeであるtext_ja_ngramと、形態素解析でインデックスの生成を行うフィールドであるtext_jaを定義しています。 <fieldType name = "text_ja_ngram" class = "solr.TextField" autoGeneratePhraseQueries = "true" positionIncrementGap = "100" > <analyzer> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <tokenizer class = "solr.NGramTokenizerFactory" minGramSize = "2" maxGramSize = "2" /> <filter class = "solr.LowerCaseFilterFactory" /> </analyzer> </fieldType> <fieldType name = "text_ja" class = "solr.TextField" autoGeneratePhraseQueries = "false" positionIncrementGap = "100" > <analyzer> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <tokenizer class = "solr.JapaneseTokenizerFactory" mode = "search" userDictionary = "lang/userdict_ja.txt" /> <filter class = "solr.SynonymFilterFactory" synonyms = "synonyms.txt" ignoreCase = "true" expand = "true" /> <filter class = "solr.JapaneseBaseFormFilterFactory" /> <filter class = "solr.JapanesePartOfSpeechStopFilterFactory" tags = "lang/stoptags_ja.txt" /> <filter class = "solr.StopFilterFactory" words = "lang/stopwords_ja.txt" ignoreCase = "true" /> <filter class = "solr.JapaneseKatakanaStemFilterFactory" minimumLength = "4" /> <filter class = "solr.LowerCaseFilterFactory" /> </analyzer> </fieldType> フィールド定義 そして、これらのfieldTypeのfieldを定義します。 <field name = "search_ngram" type = "text_ja_ngram" multiValued = "true" indexed = "true" required = "false" stored = "true" /> <field name = "search" type = "text_ja" multiValued = "true" indexed = "true" required = "false" stored = "true" /> 他のフィールドからのデータのコピー 最後に、copyFieldを使い、検索対象としたいフィールドを上記の2つのフィールドにコピーします。 <copyField source = "title" dest = "search_ngram" /> <copyField source = "title" dest = "search" /> <copyField source = "description" dest = "search_ngram" /> <copyField source = "description" dest = "search" /> ... これでインデックス生成部分の設定は完了です。 試しにAnalysis機能でこれらが正常に働いていることを確認してみます。 Ngramによるインデックス 形態素解析によるインデックス クエリの投げ方 次にこれらのフィールドに対して投げるクエリを説明します。 複数のフィールドに対して横断的に検索をするために、eDisMaxクエリを利用します。 以下のようなパラメーターでクエリを投げることで、両方のフィールドを利用して検索を行うことができます。 q=<検索したい文字列> defType=edismax qf=search 100 +search_ngram 50 qfで指定している100と50はそれぞれのフィールドの重みです。 重みが大きいほど、そのフィールドでヒットしたドキュメントが先頭に出やすくなります。 このあたりのチューニングは検索結果を見ながら値を変えてゆく必要があります。 まとめ 形態素解析とNgramの併用によって、検索漏れを減らし、検索ノイズの増加を抑制することができました。 その結果として、特にiQONではブランド名の略語で商品の検索ができるようになりました。 例えば、「JIMMY」という検索クエリで「JIMMY CHOO」の商品をヒットさせることができるようになりました。 iQONで扱う商品数は日に日に増えており、昨年10月の時点では1200万点に達しました。 ファッションアプリ「iQON」、掲載アイテム数が累計1,200万点突破! 〜新規ファッションECと提携し更なる事業拡大を加速〜 これらの膨大な商品の中からユーザーの好みに合った商品を探すことは非常に難しい課題です。 本記事で紹介した手法はこの課題に対する1つのアプローチであり、iQONではこれ以外にも様々な方法でユーザーに欲しい商品が見つかる体験を提供することを考えています。 例えば以下の記事ではディプラーニングを活用した画像ベースでの検索を紹介しています。 ディープラーニングによるファッションアイテム検出と検索 VAEとGANを活用したファッションアイテム検索システム このチャレンジングな課題を解決してみたいという方は以下のリンクからご応募ください。
こんにちは、VASILYバックエンドエンジニアの塩崎です。 今年の就職活動のスタートは3月になるそうなので、学生の皆さんは自分の進路について色々と悩んでいる時期かと思います。 今回のTECH BLOGは、Rubyの開発者であり、VASILYの技術顧問でもある、まつもとゆきひろ氏(以下、Matzさん)にエンジニアの就職活動について色々と質問した内容をお届けします。 将来エンジニアとして活躍したい学生さんの助けになればと思います。 Matzさんが学生の頃のインターン ― Matzさんが学生の頃のエンジニアの就活は今と比べてどうでしたか?今のようなインターンは当時からあったんでしょうか? インターン自体は当時からあったんですが、今のように就職に直結するものではありませんでしたね。社会経験みたいな形でインターンに行きました。内定が目的のインターンではなく、バイト感覚でした。 ― 今のインターンは内定直結型が多いですが、それについてはどう思いますか? 良いことだと思いますよ。 お互いに相手のことを知った状態で次の段階に進めるので、書類だけで選考するよりも良いと思います。書類だけを書くのが上手い人が来てもしょうがないですしね(笑) 企業側からすると、インターン生に何をやってもらうのかを決めるのは難しいことだとは思います。ですが、そのあたりを体系化できるとお互いに正直になってマッチングできるので、最高ですよね。私の頃はがっつり仕事をさせてもらえるようなインターン先ってほとんどなかったんですよ。 インターンについては今の方が良くなったと思います。 エントリーシートは最悪の発明!? ― 逆に今の方が悪くなったことは、何なんですか? それはエントリーシートですね。 今のエントリーシートって何十通も一気に出せるじゃないですか。あれって、企業と学生さんの双方にとって最悪なんですよね。企業からすると自分たちに興味がない学生のエントリーシートが送りつけられてくるわけですよね。学生さんからすると自分が出したエントリーシートが他の大勢の人のエントリーシートの中に埋もれちゃいますよね。 あれは最悪の発明ですよ。 大手の就活サイトがエントリーを無駄に急かすようなことをして軽く炎上したこともありました。エントリーシートは世の中から無くした方がいいと思います。 ― インターンは学生さんの本気度を見る手段として有効だと思いますか? 本当にそう思います。 インターンに行くってことは内定が出たら入社したいという意味ですからね。インターンの期間も2週間〜1か月くらいあって、そのくらい働くと本気になりますよね。そういう意味ではインターンはエントリーシートよりも良いシステムだと思います。 今の就活のシステムってハックする必要があって、みんなと同じ方法をしてしまうと、運が良くないと就職できなくなってしまいますよね。 学生さんでも中途として転職するのと同じようなハックが必要になってきます。学生さんのうちから情報発信をしたりとか、みんなが目指すような大企業ではないところを目指すとか。そういう、就活に対する見方を変えるようなハックをしないと、就職が辛い時代になっている気がします。 Matzさんがインターンをするとしたら? ― もしMatzさんが今の時代の学生さんだったら、どんな会社にインターンに行ってみたいですか? 大企業ではない方がいいんじゃないかなぁと思います。 今もまだありますし、私たちの頃はもっと強かったんですけど、大企業志向ってありますよね。でも今は大企業でも倒産したりとか、リストラがあったりとか、大企業に入るメリットがだいぶ減ってます。 あと、大企業では組織防衛のため、全体として保守的になる傾向があります。そのせいで、承認フローが複雑だったり、一人一人の裁量が小さかったりしますよね。そういう事情を考えると、自由を重んじるエンジニアになりたい学生さんは大企業じゃないところを目指すのがいいんじゃないかなと思います。 小さい企業だからリスクが低くなるというわけではないので、そこは見分けなきゃいけないですよね。テクノロジーに力を入れているかとか、エンジニアを大切にしているかとか。でも、現代においては大企業じゃないとこを選ぶ方が幸せになれる確率が上がる気がします。小さい会社の中で、エンジニアを大切にしていたり、テクノロジー的に面白そうなことをしているところを選ぶといいと思います。例えばVASILYさんみたいな(笑) エンジニアを人間として扱っているかも重要ですね。 エンジニアを交換可能なものとして扱っている会社って結構あるんですよね。そういうところでは技術的に突出した人はリスクになるんですよね。その人がいなくなるとプロジェクト全体が失敗してしまうので、その人の専門性を使わないという経営判断は十分ありえますね。でも、そうは言ってもその人にしかできないことがあるから任せようという判断ができる会社だと、その人の能力が生きる気がします。 Matzさんも参加してみたいというVASILYのインターンとは? ― VASILYのインターンの大きな特徴として、インターン生の作ったものをiQONに組み入れて全世界にリリースするまでを行うということがあります。もしも学生時代のMatzさんがこんなインターンの募集を見つけたら、行ってみたいと思いますか? そりゃ行ってみたいと思いますよ!! つまらないタスクを切り出してインターン生に渡したりとか、本番では使えないコードを書かされたりとか、本当の仕事とは違う作業を行うインターンが多いと聞きます。そういうインターンは実力のあるエンジニア候補にとってはだいぶ物足りないですよね。 VASILYさんのようなインターンを企画するのって難しくて、採用側にはコストが高いと思います。ですが、そこをうまく乗り越えることができれば、そのインターンはすごい魅力的に聞こえます。私が学生だったら、参加したいと思いますよ。 学生さんも色々な人がいて、中には楽してインターンの経験を得たい人もいます。でも採用側としてはそういう人よりも、本番環境に出したいという熱意のある人の方を採りたいですよね。そういうことを考えると、魅力的な対象にフォーカスしてインターンのプログラムを設計するのはすごくいいことだと思います。多くの企業がリスクを考えて深く踏み込めていない中、そこまでやっているのはすごいことだと思います。 就活生よ、偉そうになれ! ― 最後に、現在エンジニアを目指して就職活動中の学生さんにMatzさんからアドバイスをお願いします! 組織と自分との関係を対等のものとして扱うように心の中で思ってください。 会社は私にお金を払っているけど、私は会社に時間と才能を提供している。だから、どちらかが上というものではなく、対等な関係であると思ってください。そうすると、会社が理不尽なことを言った場合に無理に我慢する必要がなくなると思います。対等な関係でないと、働く側だけが我慢することになりがちです。いわゆるブラック企業ではそういう構図になっているんじゃないかと思います。でも、どちらが偉いというわけではなく、対等という意識があれば、理不尽な要求に対してNOが言えるわけですよ。 人間の心理的にお金を『貰った』とか雇って『貰った』みたいな『貰った』という言葉を使うと立場が低くなりがちですよね。入社して『やった』とか働いて『やった』ぐらいのことを心の中で思っているといいんじゃないかなと思います(笑)やや自分の方が上になりましたが、それくらいでバランスがとれるんじゃないかなと思います(笑) ちなみに、私は新人の時にはすごく偉そうな新入社員で、すぐに「こうあるべき」みたいな正論を言うし、時間の使い方はフリーダムだし、だから、上司は扱いづらかったんじゃないのかなぁと思っています。でも長い目で見るとお互いにとってプラスだったんじゃないかなとも思います。もし私が会社よりも自分の方が下という思いを持ってしまっていたら、多分Rubyは生まれてなかったかもしれませんね(笑) 「皆さんも偉そうな就活生や新入社員になってください」というのが、僕からのメッセージです! まとめ 今回のTECH BLOGはエンジニア志望の学生に向けた、まつもとゆきひろ氏とVASILY若手社員の塩崎の対談でした。 この記事が学生の皆さんにとって将来に歩む道を決める参考になれば幸いです。 1時間程の短い対談でありましたが、とても良い発言が多く、編集するときにどこを割愛すれば良いのか頭を悩ませました。 ここには書ききれなかった事もたくさんあるので、気になる方はどこかで私に会ったときに直接聞いてみてください。 VASILYでは春季インターンの募集を行っています。 この対談記事を見て興味を持った学生さんはぜひ以下のバナーからご応募ください。
こんにちは、Webフロントエンドエンジニアの権守です。 フロントエンドエンジニアの皆さんは、リリース前の社内QAにてデザイナーにピクセルのずれを指摘されて修正したという経験があるのではないでしょうか。今回はiQONのPC・スマホサイトを構築する上で、デザインデータに忠実なCSSコーディングをどのように実現しているかを紹介します。 ツールの利用 PerfectPixel 既に利用されている方も多いかもしれませんが、デザインとのピクセルずれの確認には PerfectPixel がオススメです。 PerfectPixelは、画像を透過して重ねることでデザインとのずれを確認できるブラウザ拡張です。シンプルですが、透過率の設定や画像の拡大縮小、オフセットの設定など必要な機能をしっかりと備えており、使いやすいです。 複数のブラウザ(Chrome, Firefox, IE, Safari, Opera)で提供されているというのも嬉しいところです。 重ねるとピクセルのずれや文言違いが一目でわかるのが伝わるかと思います。 Sketch Adobe Fireworksの開発が終了して以来、VASILYでは、デザインデータはAdobe Illustrator, Adobe Photoshopを使って作成されていました。 しかし、元々Webデザインを行うことを目的に作られていないこともあり、エンジニアにとって使いづらく、実装時の障害になっていました。また、サイトの大規模化に伴いUIコンポーネント管理も問題となっていました。 そこで、VASILYでは、デザインと実装の効率化を図るために昨年夏ごろから Sketch を導入し始めました。 Sketchは、WebデザインやアプリデザインなどのUIデザインに優れたデザインツールです。 シンボル機能を使ったUIコンポーネント管理や、オブジェクト間の距離の表示、ワンクリックでの画像の書き出し、デザインプロパティのCSSプロパティへの変換など便利な機能が豊富にあります。 また、有志の方々により高機能なプラグインも多く開発されている点も魅力的です。 導入にはデザイナーの協力が不可欠ですが、導入できる方にはぜひおすすめしたいです。 コンポーネント化 PerfectPixelを使っているとデザインとの小さな乖離でも気になるようになってきます。そのような乖離を減らすためには、デザイン側と実装側の両方でコンポーネント化が必要になってきます。 デザイン側はSketchの導入によって、シンボル機能を用いてのコンポーネント化を行いました。 実装側では、以前、 本ブログでも紹介 したようにSassのMixinという形でコンポーネントを管理しています。デザイン側でもコンポーネント化が進んだことによって、どこまでがpaddingでどこからがmarginかといったことがわかるなど、より適切にコンポーネントを把握することができようになりました。それによって、デザインとの乖離が減ったのはもちろん、実装時の迷いも減りました。 コンポーネントの例を以下に示します。 brand_link_list.sass @mixin m-brand-link-list( $mright , $column : 3 , $size : medium ) $link-width : 0 @if $size == large $link-width : 312px @else $link-width : 251px ul . m-brand-link-list margin-top : -4px padding-bottom : 4px li display : inline-block background : url( $cdn-domain + "shared/link_icon.png" ) no-repeat 3px 7px vertical-align : top $pleft : 15px width : $link-width - $pleft padding : 0 0 33px $pleft margin-right : $mright & : nth-child( #{$column}n ) margin-right : 0 a color : $link-color font-size : 14px line-height : 22px & : hover text-decoration : underline padding-bottom : 1px . sub font-size : 10px line-height : 16px margin-top : 3px モックデータの管理 デザインデータ上のテキストはモックが入力されています。そのデザインに合わせてコーディングを行っていると、デザインに合わせることに夢中でコードレビュー前に実際のデータに戻し忘れてしまうということがあります。 そこで、以下のようなRailsのヘルパーを実装しました。 application_helper.rb ... def apply_mock (original, model_name, key) return unless Rails .env.development? mock = YAML .load_file( Rails .root.to_s + " /config/mock/ #{ model_name } .yml " )[key] if original.is_a?( Array ) original.clear.concat(mock) elsif original.is_a?( Hash ) original.merge!(mock) end end ... config/mock/users.yml :my_page_design : :nickname : 'ゴンノカミ' :email : 'hoge@vasily.jp' mock_sample.html.slim - apply_mock( @user , :users , :my_page_design ) section h1 = "#{ @user [ :nickname ] } さんのマイページ " p = @user [ :email ] YAMLから読み込んだデータで元のデータを表示前に上書きします。試験導入中なので、実装はシンプルなものになっていますが、十分に効果を発揮しています。コミットする際にはapply_mockをコメントアウトする運用をしてますが、もし、コメントアウトし損ねてしまったとしても開発環境でしかデータを上書きしないようにしてあります。 現状は、表示側をデザインに合わせていますが、 Craft などのプラグインを使ってSketch側でデザインをAPIに合わせるという方法も有効だと思います。 Tips 最後に忠実なCSSコーディングをする際につまづきがちなポイントについて紹介します。 line-heightについて line-heightは行の高さを指定するCSSプロパティですが、このプロパティが忠実なコーディングをする際に厄介なポイントの一つです。PhotoshopやSketchなどのデザインツール上では、テキストオブジェクトは行の高さではなく行間を設定します。設定された行間をそのままCSSのline-heightに反映すると一行目と最終行で余計なpaddingが生まれてしまいます。そのため、前後のコンポーネントとのマージンをその分減らすなどの処置が必要です。 line_height_sample.html < section > < h2 > 見出し1 </ h2 > < p > 段落1です。 </ p > </ section > < section > < h2 > 見出し2 </ h2 > < p > 段落2です。 </ p > </ section > line_height_sample.sass section margin-bottom : 60px h2 $font-size : 16px $line-height : 20px $diff : ( $line-height - $font-size ) / 2 font-size : $font-size line-height : $line-height margin : - $diff 0 ( 20px - $diff ) inline-blockのリストについて リストの要素をinline-blockを使って横並びにした際に、意図しない空白が生まれてしまうことがあります。 inline_block_sample.html < ul > < li > A </ li > < li > B </ li > < li > C </ li > </ ul > inline_block_sampe.sass li display : inline-block このようにマークアップした場合に、リスト要素の間の空白が空白文字としてブラウザ側で表示されますが、本番配信時にはHTMLファイルはminifyされた状態で配信されるので、その空白の分だけ表示がずれてしまいます。 これを防ぐには開発環境でもminifyを行うか、ulに対して font-size: 0 を指定することで解決できます。 iQONではSlimを使っているので、Railsの開発環境設定に以下のように記述を追加して、minifyを行っています。 config/environments/development.rb Iqon :: Application .configure do # ... settings Slim :: Engine .set_options pretty : false end モダンブラウザであれば display: flex を使って横並びにするのもよいと思います。 まとめ 今回は、どのようにしてデザインデータに忠実なCSSコーディングを行っているかを紹介しました。綺麗なサイトを保つためには、エンジニアとデザイナー双方の協力が不可欠です。それを踏まえた上で、ツールやテクニックを利用しお互いの負担が軽くなるようにできればと思い、日々コードを書いています。本記事が、精密なCSSコーディングができなくて困っている方の助けになれば嬉しいです。 最後に VASILYでは、細部までこだわったサービスを一緒に作っていける仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 https://www.wantedly.com/projects/61388 www.wantedly.com
こんにちは、エンジニアの荒井です。 2016年はAMP(Accelerated Mobile Pages)の正式サポートがアナウンスされ、導入した方も多いのではないでしょうか。VASILYでもAMPを導入し数ヶ月運用しています。AMPの導入に関しては、関連記事も多く存在しますが、導入したことによって得られた結果について触れている記事は少ないと感じています。そこで本記事では、弊社でのAMP導入事例と、AMPによって得られた効果を紹介したいと思います。これから導入を検討している方はAMP対応による効果の一例として、すでに導入している方は比較対象として参考にして頂ければと思います。 AMPについて 初めにAMPについて簡単に説明します。 AMPはモバイルページを高速にユーザー届けるために発足した プロジェクト です。昨年Googleの検索結果に表示されるようになり、様々なサービスで対応が進められています。対応したページは以下のように「⚡AMP」と検索結果に表示されます。 AMP動作イメージ AMPは以下の手順で高速化を実現しています。 コンテンツ提供者がAMP用のHTMLを作成 GoogleのクローラーがAMPページをキャッシュ CDNにてAMPドキュメントをすべて配信 ユーザーが検索した際、キャッシュ済みのAMPページが表示される これを実現するために、AMPでは以下の3つの要素が重要となっています。 AMP HTML AMP JS Google AMP Cache AMP HTML 拡張されたHTMLです。パフォーマンスを保証するために制約が設けられています。 記述はHTMLとほぼ同様ですが、imgタグがamp-imgタグに置き換わるなど、一部のタグが専用のタグなっています。これらはAMP HTML コンポーネントと呼ばれ、AMP実装にあたり理解することが不可欠です。また、「最上位階層のタグを <html ⚡> 」にするといった、ドキュメントが満たすべき条件があります。本記事では詳細を割愛しますが、 公式のチュートリアル に必須マークアップが詳しく紹介されています。 AMP JS パフォーマンス最適化のためのJavaScriptライブラリです。ページのレンダリング速度が低下しないように非同期のJavaScriptのみが許可されています。AMPでは作成したJavaScriptを使用することが出来ないので注意が必要です。AMP JSライブラリの読み込みはheadタグの最後に行う必要があります。 < head > ・・・ ・・・ < script async src = "https://cdn.ampproject.org/v0.js" ></ script > // 必須のAMPJS ライブラリ </ head > また、使用するAMP HTML コンポーネントによっては別途ライブラリの読み込みが必要となります。 その際、必須のAMP JS ライブラリよりも前に読み込むように注意してください。 Google AMP Cache 検証済みの有効なAMPドキュメントを配信するCDNです。 AMPでは迅速なユーザー体験を提供するため、Google AMPキャッシュから直接コンテンツ配信を行います。 AMPの導入手順 それではiQONで導入した際の流れや注意点を紹介していきます。AMPを導入する際、以下の手順で進めました。 対応するページの検討 ページの構成要素を見直し 実装と動作テスト アクセス解析 マネタイズ 対応するページの検討 既存のトラフィックデータとAMPとの親和性を考え対応ページを決定しています。今回はモバイル検索から流入するユーザーの多くが求めているアイテム詳細ページを対象としました。アイテム詳細ページはすでにモバイルページが存在しているため、まずはモバイルページをベースに着手しました。しかし、前述の通りAMP独自仕様に従う必要があるためモバイルページをそのまま流用することは出来ません。いくつか大きな制約があるため、ページを構成する要素を見直しています。 ページの構成要素を見直し すでに存在するモバイルページを流用しようとした際、数々の制約の中でも以下の制約が実装に大きな影響を及ぼしました。 作成したJavaScriptを使用できない 動的なUIはAMP HTML コンポーネントで提供されているもののみ可能 linkはcanonicalだけしか許可されていない 特にJavaScriptの制限は影響が大きく、UIやページの構成要素に大きな変更を与えました。モバイルページとAMPページの差分の一例を紹介します。 上記はモバイルページとAMPページのキャプチャです。 一見同じように見えますが、メニューやカルーセルなどで違いがあります。 AMP HTML コンポーネントで実現不可能なUIを変更したり、AMPページでは不必要と判断し機能を削るなどの調整をしていきました。LIKE機能や「もっとみる」といった独自のアニメーションを用いている箇所も修正対象となっています。 このように、提供しようとしているUIがAMPで実現不可能な場合があります。実現可能かを判断するために、AMP HTML コンポーネントを把握しなくてはいけません。 AMP HTML コンポーネント iQONで使用した主要なAMP HTMLコンポーネントを紹介します。 非常に多くのコンポーネントが用意されているため、制約はあれど、ある程度のUIは実装出来ると思います。コンポーネントについては こちら のサイトで分かりやすく紹介されています。 amp-img 画像を使用している場合にはimgタグの代わりにamp-imgタグを使用する必要があります。 画像を使用するサイトは多く存在するため、AMP対応する際は必須となるコンポーネントだと思います。 < amp- img src = "sample.jpg" alt = "hoge" height = "150" width = "150" ></ amp- img > amp-carousel カルーセルを実装するために必要なコンポーネントです。carousel、slidesといった表示タイプが設定できます。slidesの場合のみですが、autoplayもサポートされています。このコンポーネントを使用するにはAMP JSを別途読み込む必要があります。 < head > ・・・ ・・・ < script async custom-element= "amp-carousel" src = "https://cdn.ampproject.org/v0/amp-carousel-0.1.js" ></ script > < script async src = "https://cdn.ampproject.org/v0.js" ></ script > </ head > < body > < amp-carousel width = "600" height = "300" layout= "responsive" type = "slides" autoplay= true delay= "2000" > < amp- img src = "sample1.jpg" alt = "sample1" width = "600" height = "300" ></ amp- img > < amp- img src = "sample2.jpg" alt = "sample2" width = "600" height = "300" ></ amp- img > < amp- img src = "sample3.jpg" alt = "sample3" width = "600" height = "300" ></ amp- img > < amp- img src = "sample4.jpg" alt = "sample4" width = "600" height = "300" ></ amp- img > </ amp-carousel > </ body > amp-analytics 従来のWebページでアクセス解析を実装しているように、AMPページでもアクセス解析が実装できます。 注意点として、現在使っているアナリティクスベンダーが 対応しているか 調べる必要があります。VASILYでは従来からGoogle Analyticsを使用しているためGoogle Analyticsで実装を進めました。 < head > ・・・ ・・・ < script async custom-element= "amp-analytics" src = "https://cdn.ampproject.org/v0/amp-analytics-0.1.js" ></ script > < script async src = "https://cdn.ampproject.org/v0.js" ></ script > </ head > < body > < amp-analytics type = "googleanalytics" id = "iqon-amp-item-analytics" > < script type = "application/json" > { "vars" : { "account" : "UA-XXXXX-Y" } , "triggers" : { "trackPageview" : { "on" : "visible" , "request" : "pageview" // 用途を指定 } } } </ script > < amp-analytics > </ body > 現在はpageviewのみトラッキングしていますが、Google Analyticsでは以下のユーザーインタラクションがサポートされています。 type 用途 pageview ページトラッキング event イベントトラッキング social ソーシャルトラッキング amp-ad AMPでも専用のコンポーネントを使用することで広告表示が可能です。 サポートされているADネットワーク をご確認の上設定してください。以下はDoubleclickの設定例です。 < head > ・・・ ・・・ < script async custom-element= "amp-ad" src = "https://cdn.ampproject.org/v0/amp-ad-0.1.js" ></ script > < script async src = "https://cdn.ampproject.org/v0.js" ></ script > </ head > ] < body > < amp-ad width = 320 height = 250 type = "doubleclick" data -ad-client= client data -ad-slot= slot > </ body > モバイルページの修正 AMPページを用意した際には、通常のページにも少し手を加える必要があります。 該当ページでAMPが用意されているか伝えるために以下のようにlinkタグを追加します。 < link href = "https://item.iqon.jp/14379365/amp/" rel = "amphtml" /> AMP側のHTMLにはcanonicalの設定が必要です。 < link href = "https://item.iqon.jp/14379365/" rel = "canonical" /> 動作テスト AMPの実装が完了したら、適切に実装されているかテストを行います。 テスト方法がいくつかありますので、普段よく使用するテストを紹介します。 AMPテストツールを使用する https://search.google.com/search-console/amp URLを送信するだけの簡単な操作です。有効なAMPページの場合は下記のように表示されます。 「検索結果をプレビュー」を押すと、実際どのように表示されるか閲覧が可能です。 エラーの場合は改善内容が表示されるのでデバッグに役立ちます。 Chrome Developer Toolsを使用する Google ChromeのDeveloper Toolsを起動し、テストを行いたいAMPページに #development=1 を付与してアクセスを行います。するとConsole上に結果が出力されます。「AMP Validation successful.」と表示されていれば問題ありません。 AMPの効果 最後にAMPの効果について紹介します。弊社サービス内で導入した結果であり、必ずしも同じような効果が期待できるとは限りません。一例として参考程度に見ていただけばと思います。 PV AMPページのPV推移を紹介します。 11月2日にAMPの導入を行いました。グラフを見ていただくと分かる通り、AMPのPVは日に日に増えています。 Google Search Consoleの「Accelerated Mobile Pagesレポート」を確認すると、リリース当日からすでにインデックス登録されていることが確認できました。導入当初はどの程度の期間を経て検索インデックスに登録されるのか未知数でしたが、予想以上に反応が良くポジティブな結果となっています。 広告 検索インデックスの登録数が増え、AMPのPVが上がるに連れ、比較的早い段階で広告による収益も出るようになりました。元々amp-adは実装していませんでしたが、PVの増加に伴い導入しました。AMPでの広告表示は遅い印象を受けますが、CTRも高くeCPMは他のモバイルページと遜色ないパフォーマンスを出しています。 運用負荷 現在、AMPページ追加による運用負荷はほぼありません。Google Search Consoleに「Accelerated Mobile Pages」の項目が追加されているので、重大な問題があった際に対応するようにしています。 AMPのViewはモバイルページと完全に分けているため、モバイルページで要素の変更があった際に、AMPも同様に変更するという工数発生が予想されます。AMPとモバイルページが同様の構成であるべきかについてはMFIの動向も含め適宜判断が必要となりそうです。 まとめ AMP導入により現状サービスにポジティブな結果が出ていると感じます。AMPの効果が不透明で導入を見送っている方の参考になれば幸いです。 2017年もモバイルファーストインデックスなど、大きな動きがあると予想されます。 一緒に盛り上げていけるエンジニアを募集しておりますので、ご興味がある方、以下のリンクから是非ご応募ください。 参考文献 AMP Project https://www.ampproject.org/ AMP by Example https://ampbyexample.com/ Google Developers https://developers.google.com/analytics/devguides/collection/amp-analytics/?hl=ja
こんにちは、Androidエンジニアの堀江( @Horie1024 )です。VASILY DEVELOPERS BLOGは新年2回目の更新になります。ちなみに去年の更新回数は53回だったようです。 また、Androidチームのトピックスとしては、先日 ベストイノベーティブアプリ大賞 を受賞した際にいただいたトロフィーがオフィスに届きました。今年も賞をいただけるようVASILY全員で頑張ります。 company.vasily.jp はじめに Android版iQONのリリースは約1~2週間に1回行っており、リリース前には社内QAを実施しています。QAを実施するために必要な作業は定型な作業ですが、手動で行うと思いの外時間が掛かりますし、作業の抜け漏れが起きる事もあります。 そこで、タイトルにもあるようにQAの実施に必要な作業を自動化しました。一度自動化してしまえば作業に抜け漏れがなくなり、自分たちの時間も節約できます。 今回は具体的にどのように自動化したのかご紹介しようと思います。 自動化の方法 VASILYでは、以前から Heroku 上にホストした Hubot を Slack と連携させ利用しています。したがって、今回もSlackとHubotを使い自動化を行っていきます。Slackから特定の文字列を入力するとHubotが反応し、QA実施に必要な作業を自動的に実行するようにします。 この記事ではHerokuやHubotの設定は紹介しませんが、Web上に良い記事が多くありますので参考にしてみてください。 QA終了までの流れの整理 まず、今までのQA開始からQA終了までの流れを整理し書き出しました。QAは毎回以下のような流れで開始され終了します。 QAの開始・終了日時をSlackで全員に通知 GoogleスプレッドシートでQAシートを作成 Qiitaチームにリリースノートを作成 APKを作成しBetaで社内に配布 SlackでQA依頼 QA終了日時を過ぎたらSlackにQA終了報告とお礼を投稿 次にこれらをどこまで自動化するのかを考えます。 自動化する範囲 どこまで自動化するかを考え易くするために先程整理したQAの流れを QA予告 、 QA準備 、 QA開始 、 QA終了 の4つのフェーズに分類してみました。 分類する中で気づいたのは、QA準備以外のフェーズは全てSlackへの投稿という点です。これらのQA実施に関する連絡は、柔軟に対応できるようにした方が使い勝手が良さそうに思えます。 例えば、QAをお願いする時に一言添えたいかもしれませんし、QAの終了日時が伸びるかもしれません。また、QAでたくさんのフィードバックをもらえてお礼の文言を変えたい場合もあるかもしれません。 したがって、 QA準備 フェーズの自動化にフォーカスします。 コマンドの設計 SlackからHubotを使い自動化するには、Hubotに反応させるコマンドを決める必要があります。シンプルに android qa としてみました。 Slackで android qa と入力するとHubotが反応し以下の作業を実行するようにします。 GoogleスプレッドシートでQAシートを作成 Qiitaチームにリリースノートを作成 APKを作成しBetaで社内に配布 どう自動化するか? QAシート、リリースノートの作成、APK配布を自動化するにはどうすれば良いでしょうか。各作業について考えていきます。 1. GoogleスプレッドシートでQAシートを作成 QAシートは以下のフォーマットを利用しています。 Sheets API を使うことでフォーマット通りのスプレッドシートを作れますが、コードが複雑になるため予め用意したスプレッドシートをマスターにし、それを Drive API でコピーしてQAシートを作ります。 追記 マスタースプレッドシートの共有にサービスアカウントを追加する必要があります。 2. Qiitaチームにリリースノートを作成 Qiitaチームへのリリースノートの作成には Qiita API v2 を使用し、 POST /api/v2/items を使うことで投稿することができます。リリースノートのフォーマットは以下の通りで、記事のタイトルには「Android バージョン番号 リリースノート」、タグには「リリースノート」、「Android」を付けます。 # リリース日 # QA * 期限 * QAシート # 更新内容 タイトルのバージョン番号やリリース日、QAの期限はSlackを通じて対話的に入力できるようにします。また、更新内容にはGitHub APIの Repository/Releases を使用し、 GitHubのリリース機能 で記載した内容を取得するようにします。そして、QAシートの項目には、作成したQAシートのURLを記載します。 3. APKを作成しBetaで社内に配布 AndroidチームではCIに Wercker を使用しています。以下のQiitaの記事にまとめていますが、Wercker APIでビルドを実行しAPKの作成とBetaでの配布を行います。 qiita.com 記事内でも紹介していますが、Wercker APIをHubotから使用するためのAPI ClientをOSSとして公開しています。 github.com 以下のように簡単にWercker APIを使用することが可能です。 const Wercker = require( 'wercker-client' ). default ; const wercker = new Wercker( { token: 'your_token' } ); wercker.Runs .getAllRuns( { applicationId: "your_application_id" } ) .then((res) => { console.log(res); } ). catch ((err) => { console.log(err); } ); 全体の流れ Slackに android qa と入力してからの処理の流れを図にすると以下のようになります。 ① Slackに android qa と入力 ② Hubotが反応 ③ Drive APIを使いQAシートを作成 ④ Qiitaチームにリリースノートを投稿 ⑤ Wercker APIを使いビルドを開始 ⑥ WerckerがGitHubからアプリのコードを取得しAPKを作成 ⑦ BetaでAPKを配布 ⑧ 配布の完了をSlackに通知 実装 上記の流れを実装したサンプルコードです。 github.com サンプルコードでは以下の流れで処理を行っています。 Google APIクライントの初期化 android qa コマンドを定義 リリースするVersion Nameを入力 マスターとなるシートをコピーしてQAシートを作成 QAシートのタイトルをVersion Nameを含めたタイトルに更新 QAシートのアクセス権限を特定のdomainに属するアカウントのみ編集できるよう変更 GitHubのrepositoryからリリースノートを取得 リリース日を入力 QA期間を入力 リリースノートをQiitaに投稿 Wercker APIを使いビルドを開始 ローカルで実行 実行中の様子はこのようになります。 リリースノートもテンプレート通りに投稿されています。 実装する上で困った点 Google APIクライントの初期化 HubotからDrive APIを操作するには google/google-api-nodejs-client を使うのですが、認証・認可についての情報が少なく困りました。 手順としては、最初にAPIマネージャーからGoogle Drive APIを有効にします。 https://console.cloud.google.com/apis/api/drive/overview 次に コンソール にアクセスし、 サービスアカウント名 と サービスアカウントID を入力してサービスアカウント新規作成します。役割には、スプレッドシートの編集を行う必要があるため 編集者 を指定します。役割の詳細は こちら から確認できます。 キーのタイプにJSONを選択し、作成をクリックするとJSONファイルがダウンロードされます。 ダウンロードしたJSONを使用し認証・認可を行います。 こちら を参考に、以下のようにgoogleapisモジュールのJWT(JSON Web Token)メソッドを使いクライアントを作成します。 JSONファイル中に client_email と private_key というフィールドがあるので、それを以下の SERVICE_ACCOUNT_EMAIL 、 SERVICE_ACCOUNT_KEY に当てはめます。 const jwtClient = new google.auth.JWT( SERVICE_ACCOUNT_EMAIL, null , SERVICE_ACCOUNT_KEY, [ 'https://www.googleapis.com/auth/drive' ] , null ); これでDrive APIを使用できるようになります。 SERVICE_ACCOUNT_KEY をHerokuの環境変数から読み込むとクライアントの初期化に失敗する場合があるので、以下の記事にまとめています。 qiita.com Hubotとの対話的な入力 リリースするVersion Name、リリース日、QA期間はHubotと対話的に入力するようにしています。今回は、 hubot-conversationモジュール を使用しました。 VersionNameを入力させるには以下のようなコードになります。 // Slackに通知 msg.reply( "リリースするVersion Nameを入力してください。" ); // Version Nameの入力を待機 var dialog = conversation.startDialog(msg); dialog.addChoice( /([0-9]*\.[0-9]*\.[0-9]*)/ , (conversationMsg) => { // 入力された文字の取得 console.log(conversationMsg.match [ 1 ] ); } ); Drive APIでのファイルのパーミッション変更方法 ファイルのパーミッションを変更するには、 Permissions の createメソッド を使用します。 パーミッションは role と type の組み合わせで指定します。例えば、編集権限をvasilyドメインのユーザーに付与するなら以下のように指定します。 { role: "writer" , type: "domain" , domain: "vasily" } google/google-api-nodejs-client では、Request Bodyを resourceパラメータで指定 するのでファイルのアクセス権限を変更するコードは以下のようになります。 drive.permissions.create( { auth: jwtClient, fileId: sheetsId, resource: { role: "writer" , type: "domain" , domain: "vasily" }} , (err, res) => {} ); まとめ 自動化した結果、QAの準備が自動的に完了するようになりとても楽になりました。また、作業の抜け漏れも無くなりスムーズにQAを行うことができています。GitHubのリリース機能でリリース内容を書くのを忘れる事があるのが課題です。 VASILYでは一緒にサービスを開発してくれるエンジニアを募集しています。今後はiQON以外の新規サービスの開発も行う予定ですので、少しでもご興味のある方のご応募をお待ちしています。