TECH PLAY

株式会社一休

株式会社一休 の技術ブログ

161

一休.com・フロントエンドエンジニアの宇都宮です。 JavaScriptを使ったWeb開発では、様々なライブラリを使います。開発の活発なライブラリであれば、毎週のようにバージョンアップが行われます。ライブラリのバージョン更新は、それを行ったからといって価値に直結するわけではありません。しかし、以下のような理由から、一定の頻度での定期更新が必要です。 バージョンアップに追従しないと、古いバージョンにロックインされる 差分が大きいバージョンアップはリスクが高い ライブラリに脆弱性が見つかった際は速やかにバージョンアップが必要 本記事では、JavaScriptライブラリ管理の標準的ツールであるnpmと、GitHub Appの Renovate を使用した、ライブラリを定期的に更新する仕組みの作り方について解説します。 npmによるパッケージ管理 npmは、JavaScriptライブラリの管理ツールです。 npmでは、使用するライブラリの名前とバージョンを package.json というファイルで管理します。 以下のように、dependencies(本番環境で使用するライブラリ)と、devDependencies(開発環境でのみ使用するライブラリ)を分け、それぞれのライブラリ名と、バージョン番号を指定します。 { "dependencies": { "vue": "2.5.16" }, "devDependencies": { "vue-loader": "14.2.2" } } npmパッケージのバージョン番号は原則として semver という仕様に基づいており、x.y.zのxがメジャー、yがマイナー、zがパッチバージョンを意味します。メジャーバージョンアップは後方互換性のない変更、マイナーは後方互換性のある新機能追加、パッチは後方互換性のあるバグフィックスです。 依存ライブラリのバージョンには、幅を持たせることが可能です。たとえば「 ^2.5.16 」は、「2.5.16以上のバージョンで、3.0未満」という意味になります。この指定方法はライブラリ向きです。他からライブラリとして参照されることのないアプリケーションでは、バージョン番号を固定しても良いでしょう。 実は、package.jsonだけでは、インストールされるライブラリのバージョンが毎回同じになることは保証されません。vue 2.5.16をインストールしたとしても、vueが依存しているライブラリのバージョンまで、全て同じになるとは限らないからです。 そのため、全ての依存ライブラリのバージョンを記録したファイルを別途用意する必要があります。npm 5以上では、 npm install の実行時に package-lock.json というファイルが生成されます。このファイルには、全ての依存ライブラリのバージョンが記録されます。 package.json と package-lock.json が存在する状態で npm install を実行すれば、正確に同じバージョンのライブラリがインストールされます。 これはnpmコマンドの代替であるyarnでも同様で、 yarn.lock に全ての依存ライブラリのバージョンが記録されています。 npmパッケージの更新手順 更新の必要なnpmパッケージを探すには、 npm outdated または yarn outdated コマンドを使用します。 yarn outdated は、semverに基づいてメジャー/マイナー/パッチの分類を行う等、よりリッチな機能を備えています。個人的には yarn outdated を好んで使っています。 バージョンアップの必要なライブラリを見つけたら、 yarn upgrade vue 等で、ライブラリのバージョンを更新します。あとは動作確認して、リリースするだけです。 簡単そうに書きましたが、実際のところ、npmパッケージの更新は面倒です。バージョンアップしても問題ないか確認するには、CHANGELOGの確認が欠かせません。 一休の宿泊予約サービス開発チームでは、週に1回程度の頻度で、以下のようなプルリクエストを作成して、npmパッケージの定期更新を行っていました。 この作業は地味に面倒で、毎週1時間程度の時間をアップデートに割いていました。そこで、パッケージ更新作業の省力化を図るための手段をいくつか検討した結果、 Renovate の導入を決めました。 Renovateとは Renovateは、GitHub上で動作するアプリ(bot)です。Renovateを導入したリポジトリでは、以下のようなプルリクエストが自動的に作成されます。 github.com ↑のスクリーンショットは、 yarn のリポジトリから取得しています。Renovateは、yarnをはじめとした様々なオープンソースプロジェクトや、 Uber 等の企業まで、幅広く利用されています。 publicリポジトリでは無料、organization + privateリポジトリでは$15/month~で使えます。更新の必要なライブラリの確認 => CHANGELOGまとめ => プルリク作成という定型作業を代わりに行ってくれるので、十分に元が取れる価格だと思います。 また、Renovate本体は OSS であり、自前でホスティングすることも可能です。 Renovateを導入する Renovateは GitHub Marketplace から導入できます。Marketplaceからrenovateを探したら、まずはプランを選びましょう。ここでは、私が個人で開発しているオープンソースプロジェクトにRenovateを導入するため、「Open Source」を選んでいます。 次の画面では、有料プランの場合、支払い情報の入力が必要になります。会社のGitHubリポジトリに導入する際は、個人アカウントの請求情報を入力しないよう注意しましょう。 最後に、インストールするリポジトリを選択します。ここでは、「ryo-utsunomiya/amazon-block」を選択しています。 1時間ほど待つと、以下のようなOnboading Pull Requestが作成されます。 github.com このプルリクでは、 renovate.json というRenovateの設定ファイルをリポジトリに追加します。デフォルトでは以下のような動作をします。 メジャーバージョンアップは別々のプルリクに分割する パッチ・マイナーアップデートを区別しない アップデート用ブランチの名前には renovate/ プレフィックスをつける マージは自動では行わない(人間が手動でマージする) package.json が更新された場合にのみlockファイルを更新する プルリクの作成は2時間毎に1回を上限とする Renovateが作成したプルリクで、マージ/クローズされていないものの数が20を超えないようにする このプルリクエストをマージすると、Renovate Botが動き始めます。 Pin Dependencies パッケージのバージョンが固定されていない場合、以下のようなプルリクエストが作成されます。 github.com ここでは、各パッケージのバージョン番号を固定(Pin)します。Pin Dependenciesプルリクをマージすると、パッケージの更新用のプルリク作成が始まります。 なお、依存ライブラリのバージョンをに幅をもたせたい場合は、以下のように renovate.json を変更すればOKです。 { "extends": [ "config:base", ":preserveSemverRanges" ] } renovatebot.com パッケージ更新の運用 Renovateによって、プルリクを作成するところまでは自動化されました。残りの作業は、安全を期して人間が行うようにしています。 具体的な手順は以下の通りです。毎週月曜の当番制にしています。 毎週月曜に、担当者がパッケージ更新の有無を確認する 更新があれば、パッケージ更新用ブランチを手元にpullして動作確認する 問題なければ、プルリクをマージする マイナー/パッチアップデートはすぐに適用しています。一方、メジャーアップデートはソースコードの改修が必要なことが多いので、担当者をアサインして、一定の時間を確保してアップデートしています。 導入の効果 Renovateによる自動化によって、プルリク作成までの手順が標準化されたため、誰でもパッケージ更新作業が行えるようになったのが大きいです(従来は、有識者が気づいたときに更新するという体制でした)。 副次的な効果として、どのようなライブラリを使用しているか棚卸しできる、ライブラリの導入時に運用コストまで考慮できる、といったものもあります。 今後の課題 更新時の動作確認に属人性があるため、バグのある状態でリリースしてしまう危険性があります。 自動テストの拡充 動作確認手順の標準化 などによって、バグのある状態でのリリースを防ぐ対策を強化していきたいと考えています。 おまけ:マイナー/パッチアップデートをまとめる デフォルト設定の場合、各パッケージ毎に更新プルリクが作成されます。しかし、週1回程度の更新頻度だと、マイナー/パッチアップデートは複数存在する状態になります。そこで、宿泊予約サービス開発チームでは、以下のような renovate.json の設定を行って、マイナー/パッチアップデートを1つのプルリクエストにまとめています。 { "extends": [ "config:base" ], "minor": { "groupName": "all dependencies" } } ↑のrenovate.jsonを使用すると、以下のようなプルリクエストが作成されます。 github.com
アバター
一休レストランiOSアプリチームのマネージャーをしているninjinkunです。自分は半年前に一休に入社したのですが、元々外食は好きだったものの、飲食に関わるサービスに関わるのは初めてでした。 そこで自分でサービスを使ったり、営業同行をしてみたりしながら、自分なりに事業領域を理解しようと試行錯誤をしてきました。このエントリではその活動の一環として読んだ本の中から、参考になったものを紹介します。 Hot Pepperミラクル・ストーリー Hot Pepperミラクル・ストーリー―リクルート式「楽しい事業」のつくり方 作者: 平尾勇司 出版社/メーカー: 東洋経済新報社 発売日: 2008/05/01 メディア: 単行本 購入 : 19人 クリック : 342回 この商品を含むブログ (39件) を見る いきなり競合他社の本なのですが、まだ紙媒体だった頃のHot Pepperの話です。立ち上げから、全国展開してアクセルを踏んでいく経緯が事業責任者の視点で書かれています。 自分にとってこの本は、一休レストランの「営業」を理解するという意味で有益でした。飲食店の集客ニーズをくみ取って、そこにHot Pepperという広告媒体を提案しに行く話が具体的に書かれているのですが、ビジネスモデルは違えど一休の営業とも共通点は多いように思います。 他にも、できる営業を分析して、全員に展開していく様は勉強になりました。営業のトークを撮影してみんなで共有したり、社内でロールプレイングをやって営業力を他の社員に展開したり、営業ツールを作ったりと、全く門外漢で知らない手法だらけだったので面白く読めました。営業の活動を知りたい開発者にはおすすめだと思います。 ミシュラン ミシュランガイド東京 2018 出版社/メーカー: 日本ミシュランタイヤ 発売日: 2017/12/01 メディア: 単行本 この商品を含むブログを見る 一休レストランはお値打ちなお店から高級なお店まで幅広いラインナップがありますが、やはり名店と言われるお店は社内での会話に良く出てきます(商品の文脈でも、プライベートで行ってみたいという文脈でも)。自分はそういった名店の知識がなかったので、定番のミシュランを買って勉強していました。掲載店が近所にあったりするのに気づくと、開拓する楽しみが増えます。 こちらはちょうど2018年版が出たところのようです。 東京カレンダー 東京カレンダー2018年1月号 出版社/メーカー: 東京カレンダー 発売日: 2017/11/21 メディア: 雑誌 この商品を含むブログ (2件) を見る 雑誌です。最近は割とゲスめのコピーを採用していることで一部で話題ですが、自分がこの雑誌が優れていると思う点は「お店の利用シーン」を明確に提案している点にあります。 実際のお店を取り上げたデートや女性同士の飲み会、謎の男女の「お食事会」などが(相当誇張されている表現ではありますが)毎号たくさん載っており、飲食店を利用するイメージが湧きます。そして何より、この雑誌のターゲット層は一休レストランのユーザー層と丸かぶりしているのです。ネタだと思われそうですが、自分は最近東京カレンダーを参考にしてユーザーストーリーを考えました。 また、社内にもファンが多いようで、最近作られた社内Slackの #東京カレンダー はやたらと盛り上がっており、同僚とのコミュニケーションにも一役買っています。 おわりに 以上、一休レストランの事業領域を理解するのに役だった本たちでした。飲食店に関する本でおすすめがあれば教えていただけると嬉しいです。
アバター
この記事は 一休.comアドベントカレンダー2017 の25日目です。 一休レストランiOSアプリ を開発している id:ninjinkun です。 iOSでは今年の夏からiTunesConnectにて段階的リリース(Phased Releases)が導入されています。段階的リリースとは、アプリの新バージョンを提供する際に一部のユーザーから順にアップデートを適用していくリリース方法です。不具合があった際に全ユーザーに影響が及ぶことを防ぐことができ、リスクを低減することができます。 この機能にの詳細については、 iOSの段階的リリースについての注意点 - inFablic | Fablic, inc. Developer's Blog. が詳しく、本エントリは追試 + 以前に使用した経験があるAndroidの段階的リリース(Staged Rollout)との比較になります。 結論を先に書くと、この機能は 段階的自動アップデート と呼ぶ方が適切だと思いました。そして使いどころが難しかったので、今後はあまり使わないでしょう。当初はAndroidと同じ機能を期待していたのですが、だいぶ違うものでした。 この情報は2017年12月時点のものなので、来年には状況が変わっている可能性がある点にご留意ください。 日数と公開割合が固定 📅 この機能を使う際には、iTunesConnectから段階的リリースを選択します。 まず気になるのが、段階的リリースの日数が固定されている点です。 7日間に渡って1日ずつ固定の割合で公開範囲が広がります 。Androidの場合は公開の段階を引き上げる日と、公開する割合を自分たちで決めることができます(ただし手動)。 手動アップデートと新規ユーザーは全員新バージョン 🆕 もう一つ注目したいのが、段階的リリースの最中であっても、ユーザーが手動でAppStoreから新バージョンをダウンロードできるという点です。さらに、 新規ユーザーは全員に新しいバージョンがインストールされます 。 Androidの場合は、公開範囲に含まれなかったユーザーは新バージョンに手動でアップデートすることができません。また、ここはきちんと検証できていないのですが、新規ユーザーについても新バージョンが適用される比率は同じであると思われます。 制御されているのは自動アップデートのみ 🆙 iOSの段階的リリースとAndroidの段階的リリースはどうやら根本的に実装方法が異なるようです。具体的には、iOSは 新バージョンの自動アップデートを制御 しているだけであり、自動アップデートではない手動アップデートと新規ユーザーの場合には新バージョンが適用されます。そしてどうやら、AppStore上に置かれるバイナリは新バージョンのみのようです。 他方Androidは 新バージョンのバイナリを適用するユーザーを制御 しており、自動/手動アップデート、新規/既存にかかわらず、公開範囲に含まれたユーザーしか新バージョンをインストールできません。そしてGoogle Play Store上には旧バージョンと新バージョンの両方のバイナリが配置されます。 ロールバック不可 🙅 この違いから、iOSの段階的リリースでは新バージョンに問題が発生しても、ストアが旧バージョンを保持していないためロールバックができません。できるのは自動アップデートの適用範囲を固定の状態で止めることだけです。その間にも新規ユーザーは問題がある新バージョンをダウンロードし続けることになります😓 修正リリースは再審査 👨‍⚖️ 修正したバージョンをリリースする際は再度AppStoreの審査があります。このため、一般的に段階的リリースの目的の一つであると思われる、不具合のリスクを抑えながら頻繁にリリースを行ってスピード感を出す目的には使えません。 また、修正したバージョンを提出する際に段階的リリースを選択すると、また1%からやり直しです。今回の使用では、前回の段階的リリース当選ユーザーに新バージョンが配布されているかは確認できませんでしたが、おそらく実装されていないのではないかと睨んでいます。 Androidはこの部分でも先行しており、段階的リリースの対象ユーザーのみに修正バージョンを配布することができます。 一休レストランでの利用事例 一休レストランでは、予約情報を入力する画面のリニューアルを行う際に、リスク低減の目的で段階的リリースを使用してみました。そして危惧していたとおり、リリース後に一部のレアなケースで予約ができないという不具合が見つかりました(事前に可能な限り確認はしていたので、発見できなかったのは仕方なかったと考えています。損害も軽微でした)。 ここで段階的リリースやってて良かった…となると良いのですが、実際は不具合が新規ユーザーのみに起こるものだったため、段階的リリースは全く意味が無かったという結果になりました 😭 おわりに 以上、Androidとの比較を交えながらiOSの段階的リリースについて解説しました。 自分の意見としては、 新規ユーザーには新バージョンが渡ってしまう ロールバックができない 修正リリースがやりづらい という点から、積極的に使うことはないと思います。今後のアップデートに期待しています。 最後にiOSとAndroidの比較について表にまとめておきます。   iOS Android 呼称 Phased Releases Staged Rollout 公開日程 7日間で固定 手動で随時変更 公開範囲 1%, 2%, 5%, 10%, 20%, 50%固定 0.5%, 1%, 5%, 10%, 20%, 50% から選択 任意 段階的リリース中の全公開 できる できる 段階的リリースのサスペンド できる(ただし手動アップデートは可能 & 新規ユーザーには新バージョンが出続ける) できる 制御できるユーザー 自動アップデートユーザー 全ユーザー ロールバック できない できる(ただし新バージョンをインストールしてしまったユーザーは戻せない。審査がないので上げ直しで対応) 対象ユーザーへの修正版配布 不明(おそらくできない) できる ストアに置かれるバイナリ 最新版のみ 新旧複数バージョン それでは、今年も一年おつかれさまでした。新年会のご予約に、一休.comレストランをぜひご利用ください。 レストランを簡単予約!一休.comレストラン IKYU Corporation フード/ドリンク 無料 2018/1/11 追記 Androidの段階的リリースの公開範囲は、現在は任意の%で指定できるようになっているとのことです。ご指摘感謝。
アバター
メリークリスマスイブ! 皆様クリスマスイブはいかがお過ごしですか。 データサイエンス部所属のエンジニア 笹島 id:sisijumi です。 年末ということもあり、今日は一休という会社のエンジニア組織の変遷を振り返るとともに、現状に関してもお話させていただきます。 (現状はデータサイエンス部ですが、今年の10月までは宿泊事業部のエンジニアのマネージャーをやっておりました。) 一休における収益の変遷 宿泊事業に関してですが、過去発表した資料を元に説明させていただきます。 (近年非上場になった為、直近の収益に関しては資料はありません。) 創業期、成長期を経て、一時停滞したものの、きちんと再度成長していることがわかりますね。 事業のステージに合わせて変化してきました 創業期、成長期を支えてきた組織構造 各部署内に宿泊・レストランチームが存在しています 各部署が都度エンジニアのマネージャーとコミュニケーションと会話して開発を進める、というスタイルになっています。 この頃、中心となるマネージャーがボトルネックになってしまうと開発の全体のスピードが遅れるという課題がありました。 また、宿泊予約やレストラン予約等それぞれのサービスがより成長し、上記の開発スタイルがそのスピード感にマッチしなくなってきました。 再成長を支えた事業部の設立 それぞれのサービスをより成長させることへコミットできるように、一休という会社としても事業部制への移行を実施しました。 それとともに事業部毎にエンジニアを配置する形に変更になっています。 事業責任者とエンジニアのマネージャーでプロダクト開発優先順位を意思決定し、開発メンバーを調整するという流れでした。 以前の形と異なる点は、エンジニアのリソースをどう活用すれば事業の成長に最も寄与できるかをきちんと意思決定しながら開発を進めている点です。 (以前の形では依頼ベースで開発を進めてしまうところがあり、事業の成長に最も寄与するためのエンジニアのリソースの配置をどうするか、という視点が欠けていたと思っています。) 目的型組織への移行 ミッション毎にプロダクト開発チームの枠組みを作りました 事業を成長させるためのミッションを作成し、ミッション毎にチームを配置しました。この形にすることで、それぞれのチームごとにミッションに集中できる構造になっています。 以前の形ですとビジネスオーナーとエンジニアの間にエンジニアのマネージャーが間に挟まってしまい、不要なコミュニケーションが発生してしまっていました。ビジネスオーナーとエンジニアの距離をより近づけることで、開発時のコラボレーションがより活発になることを目指しました。 事業部長が作成したミッションごとのチームそれぞれで、プロダクト開発が進む体制になっています。 この構造においては、マネージャーはエンジニアを後方支援する形になっています。また事業部長とのコミュニケーションも何をやるかではなく、事業の成長に各ミッションへのエンジニアのリソースは最適化できているか、という視点が主になっています。 最後に エンジニアの枠組みとしてこうあるべき、というビジョンも必要ですが、それに囚われずチーム構造を柔軟に変化させる必要があります。 自分がマネージャーだと、現状のチーム構成から変化させることに心理的負荷はやはりあります。(チームでやってもらっているメンバーにきちんと説明し、合意をした上できちんと進めないといけないということが頭をよぎり、反射的に反対することもある) しかし、今の立場に縛られず現状の事業をどうやって伸ばすか、成果を最大化させるためにエンジニアチームでやるべきことはなんなのか、など事業にきちんと貢献できているかを客観的に継続的に判断する必要があります。事業的な成長をプロダクト開発において牽引していきたいと思います。 明日は id:ninjinkun の iOSとAndroidの段階的リリース機能を比較する です
アバター
本記事は、 一休.com Advent Calendar の23日目です。 宿泊事業本部フロントエンドエンジニアの宇都宮です。 先日(12/19)、一休の宿泊予約サービス(以下、一休.com)のスマホ版の予約入力画面リニューアルをリニューアルしました。本記事では、 どのような方針で どのような技術を使って どのような設計で どのように実装していったか を紹介します。 Before/After リニューアル前 ファーストビュー エラー通知 次の画面へ進む前に、alert() リニューアル後 ファーストビュー エラー通知 項目の上に表示&自動フォーカス 主な変更点 デザインの刷新 エラー通知の改善 クレジットカード入力画面、オプション注文画面の統合 実装面では、ASP.Net Web Formsのユーザーコントロールを大幅に削り、Vue.jsのコンポーネントに置き換えました。 以前の画面の課題 リニューアル前の予約入力画面には、いくつかの課題がありました。 UIが使いにくい 特にエラー通知が不親切 画面の表示が遅い(サーバサイドで大量のデータを読み込んでいるため) ユーザーコントロールの変更が難しく、生産性が低い エンジニアの観点からすると、特に生産性の低さが気になっていました。ユーザーコントロールとはASP.Net Web Formsフレームワークの機能で、再利用可能なコンポーネントを定義する機能です。 ユーザーコントロールは、上手に活用すれば生産性を向上させる機能だと思いますが、残念ながら一休の予約入力画面では、生産性を低下させる原因となっていました。というのも、数千行規模のユーザーコントロールが複数存在し、それぞれが密結合していたのです。 4月に、フォームの入力欄の順番を入れ替える作業を行いました。表面上は簡単な調整で、普通のHTMLなら30分でできそうな作業でしたが、実装1日+テスト0.5日という工数を費やす必要がありました。 あるユーザーコントロールから別のユーザーコントロールに入力欄を移動するには、その入力欄のために定義されているフィールド/プロパティやメソッドを、まるごと別のユーザーコントロールに移す必要があったためです。 ↓は当時のプルリクのスクリーンショットですが、3項目の移動で+3,571 −166という大きなdiffが生じています。 また、予約入力画面のaspxファイルは単体で5,000行(※)ほどあり、ファイル内で行きつ戻りつするのも非常に大変でした。 ※:JSが3,000行、残りはHTMLとASP.Netのコードナゲット このような理由から、予約入力画面のリニューアルを検討しました。 リニューアルの方針と実装 スコープ 予約の画面は膨大な業務ロジックに支えられているため、実装工数が膨らむことは避けられません。そこで、以下のようにスコープを限定して進めました。 対象はスマホ版の予約入力画面のみ PC版や、スマホ版でも予約確認画面や完了画面には手をつけない サーバサイドの業務ロジックにはできるだけ手をつけない。レスポンス速度の改善はスコープ外 技術選定 予約入力画面のリニューアルでは、EFO(Entry Form Optimization)の観点から、JavaScriptでリッチなエラー通知を行うことを目指しました。あわせて、開発者の生産性を向上させる必要性も感じていました。そこで、JavaScriptフレームワークのVue.jsを導入しました。 既存の画面で使用しているjQueryではなく、Vue.jsをメインに据えた理由は、生産性が大きな要因です。Vue.jsの 単一ファイルコンポーネント を使用すると、画面上の要素(コンポーネント)ごとに、関連するロジック(js)とスタイル(css)をひとまとめにすることができます。コンポーネントを小さく保ちながら実装を行えば、コードのメンテナンス性を大きく向上させることができると考えました。 実装 Vue.js + Vuex + VeeValidateというスタックで実装を行いました。 Vuex を導入するかは迷いましたが、予約入力画面で使用する膨大なデータを一元管理できるのは大きなメリットでした。 VeeValidate はpluggableな構成になっていて、独自のバリデーションを簡単に定義できるようになっているのが良かったです。 テストには vue-test-utils を使用しています。Vueコンポーネントのテストは一部のメソッドと算出プロパティだけで、レンダリングのテストは行っていません。 現在のところ、コンポーネントの構成は以下のようになっています。 大枠から作っていって、徐々にコンポーネントを分割していくという方針を採ったため、コンポーネントの粒度は大きめです。また、本来はコンポーネントにすべきなのに、コンポーネント化できていない部分もあったりします(たとえば、お知らせメール受信設定)。 目安として、.vueファイルが100行を超えたらコンポーネントを分割できないか検討するようにしていました。最も大きなコンポーネントで.vue 200行 + .js 300行ほど、平均的なコンポーネントのサイズは100行程度に抑えることができたため、コードの見通しはかなり良くなりました。 また、以前は5,000行ほどあった予約入力画面のaspxファイルは、リニューアルによって200行にまでスリム化しました。 残る課題 本ページのリニューアルに関しては一区切りというステータスですが、以下のような課題が残っていると認識しています。 一休.com全体でのデザイントーンがバラバラ 画面の表示は相変わらず遅い コンポーネントの粒度はもう少し細かい方がよさそう JavaScriptの単体テストが不足している CSS Modules がまだ使いこなせてない デザイン面の課題はデザイナーと協力して、プログラムの課題は他のエンジニアとも認識を合わせながら、引き続き改善していきたいと考えています。 明日はzimathonさんの「開発組織の目的型組織への移行」です。
アバター
この記事は 一休.comアドベントカレンダー2017 の22日目です。 いよいよ今年も終わりですね。 みなさん クリスマス の、 忘年会 のご予約はすみましたか? というわけでアドベントカレンダー2打席目、 一休.comレストラン 検索 & 集客担当のにがうりです。 一休の本社は赤坂見附の駅からほど近くにあり、お昼ごはんの選択肢が非常にバラエティに富んでいるのが嬉しいところです。 もちろん、その中には一休.comレストランにご加入いただいている店舗様もたくさんあります。 本エントリでは 筆者のお昼休み中に通える範囲内にあり 一休.comレストランでランチが予約できる レストランがどのくらいあるのか、Solrの空間検索( Spatial Search )を利用して調べてみました。 なお、 前回のエントリ 同様、Solrのバージョンは7.1.0を前提としています。 事前準備 Solrのスキーマ構成 ひとまず、以下の項目を用意します (restaurant_idがuniqueKey) <field name = "restaurant_id" type = "string" indexed = "true" required = "true" stored = "true" /> <!-- レストランのID --> <field name = "restaurant_name" type = "string" indexed = "true" stored = "true" /> <!-- レストランの名称 --> <field name = "lat_lon" type = "location" indexed = "true" stored = "true" /> <!-- 緯度、経度 --> <dynamicField name = "*_coordinate" type = "pdouble" indexed = "true" stored = "false" /> 対象となるデータ 「赤坂・永田町・虎ノ門」エリアで12時に予約できるお店が 120件ほど 見つかりました。 これらの店舗のID, 名前, 緯度経度をSolrに登録しておきます。 Solr検索のテスト 試しに3件取得 URL http://localhost:8888/solr/ikyu-advent-2017-spatial/select?wt=json&echoParams=none&fl=restaurant_id,restaurant_name,lat_lon&q=*:*&rows=3 取得結果 無事に登録されていました { responseHeader : { status : 0 , QTime : 5 } , response : { numFound : 120 , start : 0 , docs : [{ restaurant_id: " 100152 ", restaurant_name: " 春秋 溜池山王店 ", lat_lon: " 35.6736091,139.740132 " } , { restaurant_id: " 100195 ", restaurant_name: " 沖縄懐石 赤坂潭亭 ", lat_lon: " 35.6685154,139.732890 " } , { restaurant_id: " 100197 ", restaurant_name: " 赤坂浅田 ", lat_lon: " 35.6738200,139.738229 " }] } } オフィスの位置を中心に範囲検索 Spatial Searchのgeofilt を使い、 オフィスの場所を中心に 半径400メートル以内の店舗を 近い順に 探してみます URL http://localhost:8888/solr/ikyu-advent-2017-spatial/select?wt=json&echoParams=none&fl=restaurant_id,restaurant_name,lat_lon,geodist:geodist()&spatial=true&sfield=lat_lon_rpt&q={!geofilt}&pt=35.675471,139.737937&d=0.4&sort=geodist()%20asc&rows=3 ※ pt=35.675471,139.737937 が一休本社オフィスの緯度経度、 d=0.4 が400メートル以内という指定です 取得結果 { " responseHeader ": { " status ": 0 , " QTime ": 0 } , " response ": { " numFound ": 36 , " start ": 0 , " docs ": [{ " restaurant_id ": " 100729 ", " restaurant_name ": " 個室会席 北大路 赤坂茶寮 ", " lat_lon ": " 35.6752587,139.738609 ", " geodist ": 0.06510001 /* geodist は中心地からの距離 ( km ) */ } , { " restaurant_id ": " 106301 ", " restaurant_name ": " 土佐料理 祢保希 赤坂店 ", " lat_lon ": " 35.6751768,139.737050 ", " geodist ": 0.086561576 } , { " restaurant_id ": " 107172 ", " restaurant_name ": " 赤坂 金舌 ", " lat_lon ": " 35.6746016,139.737555 ", " geodist ": 0.10269355 }] } } 無事取得できました。 一見、後は半径を調整すればいけそうに思われます・・・が、一休本社の周辺マップは このように なっています。 ※ 赤いピンが株式会社一休本社 ご覧の通り、 東側には大通り 大通りを抜けても日枝神社や大使館 北側もガーデンテラスの方向に抜けるためには何回か信号を渡る必要がある となっているため、単純な半径の調整で良い具合に、というのは中々厳しいものがあります。 ここは、円形ではなく任意の範囲を指定して検索したいところです 任意の範囲を指定して検索 任意の範囲を検索するためには、 どうやって任意の範囲を指定するのか 任意の範囲を使った検索をどのように行うか という2つの課題をクリアする必要があります。 幸い、1は Googleマイマップ 、 2は JTSを利用したSpatial Search を使うことで対応できました。 Googleマイマップで範囲データを作成 Googleマイマップでは地図上に自由にラインを引くことでき、さらにそれを KML 形式のデータとして出力することが可能です。 ※ Googleマイマップの使い方については本稿の主旨と異なるため、割愛します ランチタイムの徒歩行動圏をこのような枠線で表現しました。 こちらをKML形式でエクスポートした結果が以下です。 ikyu-advent-2017-spatial.kml <? xml version = "1.0" encoding = "UTF-8" ?> <kml xmlns = "http://www.opengis.net/kml/2.2" > <Document> <name> ikyu-advent-2017 </name> <Style id = "poly-000000-1200-77-nodesc-normal" > <LineStyle><color> ff000000 </color><width> 1.2 </width></LineStyle> <PolyStyle><color> 4d000000 </color><fill> 1 </fill><outline> 1 </outline></PolyStyle> <BalloonStyle><text> <![ CDATA [ <h3>$[name]</h3> ]]> </text></BalloonStyle> </Style> <Style id = "poly-000000-1200-77-nodesc-highlight" > <LineStyle><color> ff000000 </color><width> 1.8 </width></LineStyle> <PolyStyle><color> 4d000000 </color><fill> 1 </fill><outline> 1 </outline></PolyStyle> <BalloonStyle><text> <![ CDATA [ <h3>$[name]</h3> ]]> </text></BalloonStyle> </Style> <StyleMap id = "poly-000000-1200-77-nodesc" > <Pair><key> normal </key><styleUrl> #poly-000000-1200-77-nodesc-normal </styleUrl></Pair> <Pair><key> highlight </key><styleUrl> #poly-000000-1200-77-nodesc-highlight </styleUrl></Pair> </StyleMap> <Placemark> <name> ランチエリア </name> <styleUrl> #poly-000000-1200-77-nodesc </styleUrl> <Polygon> <outerBoundaryIs> <LinearRing> <tessellate> 1 </tessellate> <coordinates> 139.7344959,35.6802109,0 139.7360516,35.6778317,0 139.7344422,35.6766465,0 139.7351772,35.6739926,0 139.7361803,35.6719924,0 139.7388196,35.67116,0 139.7404772,35.672341,0 139.7392058,35.673352,0 139.7372103,35.680516,0 139.7344959,35.6802109,0 </coordinates> </LinearRing> </outerBoundaryIs> </Polygon> </Placemark> </Document> </kml> 「coordinates」の中に経度、緯度の羅列が入っています。こちらが枠線の情報のようです。 ちなみに、この地図に対して前述の120件のレストランをプロットするとこのようになりました 範囲広すぎましたね・・・ 気を取り直して続けます。 検索の下準備 インストール直後のSolrでは空間検索ができない状態でした。 空間検索を行うためには、JTSを入手する必要があります。 JTSの入手 & 設定 https://repo1.maven.org/maven2/com/vividsolutions/jts-core/ より jts-core-{バージョン}.jar をダウンロード (本稿作成時点では1.14.0) SOLRインストールディレクトリ/server/solr-webapp/webapp/WEB-INF/lib/ にコピー ※ SOLRインストールディレクトリ/server/lib と間違えないように SolrにJTSを反映 1. 定義済みの型「location_rpt」に対し spatialContextFactory="JTS" を追記 (これをやらないとエラーになります) <!-- 変更前 --> <fieldType name = "location_rpt" class = "solr.SpatialRecursivePrefixTreeFieldType" geo = "true" maxDistErr = "0.001" distErrPct = "0.025" distanceUnits = "kilometers" /> <!-- 変更後 --> <fieldType name = "location_rpt" class = "solr.SpatialRecursivePrefixTreeFieldType" geo = "true" maxDistErr = "0.001" distErrPct = "0.025" distanceUnits = "kilometers" spatialContextFactory = "JTS" /> 2. location_rpt型の列を追加 lat_lon_rptという列を追加しました。 データを作り直すのは面倒なので、copyFieldでlat_lonの値をコピーさせています <copyField source = "lat_lon" dest = "lat_lon_rpt" /> <field name = "lat_lon_rpt" type = "location_rpt" indexed = "true" stored = "true" /> 3. Solr再起動 この後データを再度登録し、lat_lon_rptにデータが入っていることを確認して下準備は完了  任意の範囲を検索 説明 を読むと、 &q= : &fq={!field f=geo}Intersects(POLYGON((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30))) という指定で範囲の指定ができるようです。 つまり、Intersects(POLYGON((...))) の中に、先のkmlのcoordinatesの内容を使えばいけそうです。 手で加工するのも面倒なので、Pythonの力を借りて検索しちゃいましょう。 ikyu-advent-2017-spatial.py (Python3.6.xで実行) import re import urllib.request import urllib.error import xml.etree.ElementTree as et def main (): root = et.parse( 'ikyu-advent-2017-spatial.kml' ).getroot() # 先程のkmlファイルを読み込み polygon = root.findtext( ".//{http://www.opengis.net/kml/2.2}coordinates" ) # coordinates内の文字列を取得 polygon = re.sub( r'\s+' , ' \n ' , polygon) # スペースをトリミング polygon = re.sub( r',0$' , '' , polygon, flags=re.MULTILINE) # 行末の.0を削除 lon_lats = [_.split( "," ) for _ in polygon.split( ' \n ' )] lon_lat_str = "," .join([f '{_[0]} {_[1]}' for _ in lon_lats if len (_) == 2 ]) # lon1 lat1,lon2 lat2,lon3 lat3... の組み合わせの文字列を生成 query = ( ( 'wt' , 'json' ), ( 'echoParams' , 'none' ), ( 'rows' , '120' ), ( 'fl' , 'restaurant_id, restaurant_name,lat_lon' ), ( 'q' , '*:*' ), ( 'fq' , f '{{!field f=lat_lon_rpt}}Intersects(POLYGON(({lon_lat_str})))' ), ) url = "http://localhost:8888/solr/ikyu-advent-2017-spatial/select?" + urllib.parse.urlencode(query) print (url) print ( "-------------------" ) with urllib.request.urlopen(url) as req: try : response = req.read().decode( 'utf-8' ) print (response) except urllib.error.HTTPError as e: print ( "HTTPError" ) print (e.reason) except Exception as e: print (e) if __name__ == '__main__' : main() 出力されたURL http://localhost:8888/solr/ikyu-advent-2017-spatial/select?wt=json&echoParams=none&rows=120&fl=restaurant_id, restaurant_name,lat_lon&q=*:*&fq={!field f=lat_lon_rpt}Intersects(POLYGON((139.7344959 35.6802109,139.7360516 35.6778317,139.7344422 35.6766465,139.7351772 35.6739926,139.7361803 35.6719924,139.7388196 35.67116,139.7404772 35.672341,139.7392058 35.673352,139.7372103 35.680516,139.7344959 35.6802109))) 取得結果 45件取得されました { " responseHeader ": { " status ": 0 , " QTime ": 1 } , " response ": { " numFound ": 45 , " start ": 0 , " docs ": [{ " restaurant_id ": " 100197 ", " restaurant_name ": " 赤坂浅田 ", " lat_lon ": " 35.6738200,139.738229 " } , { " restaurant_id ": " 100729 ", " restaurant_name ": " 個室会席 北大路 赤坂茶寮 ", " lat_lon ": " 35.6752587,139.738609 " } , /* ---- 略 ---- */ { " restaurant_id ": " 107172 ", " restaurant_name ": " 赤坂 金舌 ", " lat_lon ": " 35.6746016,139.737555 " } , { " restaurant_id ": " 107347 ", " restaurant_name ": " ビストロMATSU ", " lat_lon ": " 35.6760843,139.735606 " } , /* ---- 略 ---- */ { " restaurant_id ": " 108549 ", " restaurant_name ": " 鉄板焼877 ", " lat_lon ": " 35.676792,139.73558 " } , { " restaurant_id ": " 108862 ", " restaurant_name ": " 焼肉しゃぶしゃぶシャンボール ", " lat_lon ": " 35.6745112,139.736611 " }] } } この45件のみで地図にプロットしなおすとこの通り。 見事、徒歩圏内のレストランのみに絞り込むことに成功しました。 45件、コンプリートの道は遠そうです。 まとめ 範囲指定の情報の作成はGoogleのマイマップを使うと楽 任意の範囲で検索するためにはJTSが必要 KMLデータをSolrの検索条件に変換する処理はPythonなりで自動化可能 この結果を活かして、検索をもっと使いやすく、便利にしていきたいと思います。 明日は id:ryo-utsunomiya さんの「一休.comスマホ版予約入力画面リニューアルの舞台裏」です。
アバター
この記事は 一休.comアドベントカレンダー2017 の 21日目です。 qiita.com 一休コンシェルジュ(https://www.ikyu.com/concierge/) のディレクターをしている id:aitamx です。2017年1月に一休にJoinし、もうすぐ1年となります。 アドベントカレンダーの一枠をいただいたので、『月1でのリリースサイクルの回し方』的な投稿も考えましたが、 それは別の機会とし、メディアっぽい内容のエントリーにしました。 箸休め的な形で愉しんでいただければ幸いです。 今回は一休.com掲載施設で、オフサイトや開発合宿、ハッカソンといったエンジニアが使うシーンに合いそうな宿をご紹介します。 \オフサイト・開発合宿で必要な設備/ 団体旅行にも。千葉の「バケーションレンタル(高級貸別荘・コンドミニアム)」 箱根のリゾートホテルでオン&オフの滞在 友ヶ島、淡路島、四国を望む絶景宿でパワーチャージ まとめ \オフサイト・開発合宿で必要な設備/ ◆ 何はなくともWi-Fi環境 ◆ プロジェクタ・モニター・ホワイトボード ◆ 非日常な滞在 オフサイトミーティングや開発合宿をするにあたり、自分たちで機材を持ち込む方もいらっしゃると思いますが、とはいえホワイトボードを持ってくのは何かとツラいですよね。 それに何かとかさばる荷物は少しでも減らしたいところ。 また、「非日常な滞在」=絶景やオシャレなインテリア、美味しい空気の中で得られるフレッシュな感覚は、クリエイティビティを必要とされる開発者の皆さんにこそ体験していただきたいので、必要項目に入れました。 しかし、条件をクリアした施設は、探してみると意外にありません。 \ マ ジ か マ ジ で か / 焦りながら結構な時間を費やし、営業の方にもアドバイスをいただき、バケーションレンタル(貸別荘)・リゾートホテル・旅館の3施設を厳選しました。 団体旅行にも。千葉の「バケーションレンタル(高級貸別荘・コンドミニアム)」 Cairns House (ケアンズハウス) 【おすすめポイント】 ・とっても豪邸 ・「ホワイトボード」「スクリーン」の貸出付きの宿泊プランがある ・一棟貸し切りなので、会議室代がかからずに作業に没頭できる こちらは千葉の館山に位置する施設で、一棟貸し切りなので自分たちが自由にアレンジできます。 とてもゴージャス&オシャレな空間で、柔軟なアイデアが生まれそう。 別館を含めて利用すると、最大15名利用ができます。 人数少なめなスタートアップ企業の合宿にいかがでしょうか。 ▼Cairns Houseについての詳しい紹介記事 こんなお家に住みたい!が叶う、憧れの豪邸 | 一休コンシェルジュ 箱根のリゾートホテルでオン&オフの滞在 ハイアット リージェンシー 箱根 リゾート&スパ 【おすすめポイント】 ・広くて居心地のよい客室で、プライバシーが保たれる ・「ホワイトボード」「スクリーン」のある会議室では、窓越しに明星ヶ岳の大文字を見渡せる ・付属のレストランが美味しい 強羅の自然を感じられるスパリゾート。上質な空間で、さすがハイアットといった安心感があります。 56平米という広々とした開放的な客室は、ゆったりくつろげる雰囲気。雑魚寝苦手な方が多いときには、ホテルは気楽です。 ホテル内のレストラン&バーが複数あるので、チームメンバーの好みに合わせたランチやディナーが楽しめるのもいいですね。 エグゼクティブな雰囲気に包まれ、濃縮したパワーミーティングをしたい方に。 友ヶ島、淡路島、四国を望む絶景宿でパワーチャージ 休暇村 紀州加太 【おすすめポイント】 ・紀淡海峡を望む絶景で湯ったりできる ・最大100名利用可能な和室の会議室 (ワイヤレスマイク・ホワイトボード・プロジェクター・スクリーン完備) ・紀州の旬の食材を使った豪快な海の幸を満喫 高級宿泊施設のイメージがある 一休.com ですが、実は休暇村の予約もできることはご存知でしょうか。 こちらの「休暇村 紀州加太」は開放感のある眺望と、四季を感じる海の幸を堪能できる施設です。 露天風呂は最近はやりの「インフィニティ風呂」! 雄大な自然の中で煮詰まった頭をリフレッシュしたら、インスピレーションがわきそうですね。 まとめ 「オフサイト・ミーティング」といっても、各企業で解釈がさまざま。 「組織力向上」や「目標・ミッション理解」、「事業部毎の課題解決」を目的とし、チームビルディングに活用されている企業も多いと聞きます。(一休でも実施しています) 業務中の会話はSlackやhipchatだけということもあるかもですが、たまに場所を変えてミーティングをすると思わぬ発見があるかもしれません。 また、今回改めて施設を探しましたが、国内にはまだまだ開発合宿向きの施設が多くないと感じました。(特に、プロジェクター+ホワイトボード+Wi-Fiの貸し出しに対応している施設が少ない) 貸別荘等を利用するケースも多いかと思いますが、自分たちで何でもしないといけないのって結構大変だったり…。 新サービスのアイデアブレストや、プロトタイプ作成、興味のある技術やトレンドをみんなで味見…など、開発合宿といっても目的はひとそれぞれ。 ニーズに合わせた宿選びができるよう、おすすめ施設を見つけたら、定期的にご紹介していこうと思います。 明日はnigauryyyさんの「 KMLを元にしたSolrの空間検索に挑戦 」です。
アバター
この記事は 一休.comアドベントカレンダー2017 の20日目です。 データサイエンス部所属のエンジニア 笹島 id:sisijumi です。 今日はクラウド環境へのデータ分析基盤構築にまつわるお話をさせていただこうと思っています。 データ分析基盤の構築に関して 夏にデータ分析基盤を Azure SQL Data Warehouse を中心にした構成で構築 構築はしましたが、残念ながらこの構成での運用には至りませんでした。 一休では元々社内にデータ分析基盤を構築し運用していましたが、運用負荷の増大に伴いその基盤のクラウド環境への移行を進めました。 下記は今年の8月のあるイベントでの発表資料ですが、イベントではデータ分析基盤は Azure SQL Data Warehouse を中心としたものに と話しさせていただきましたが、現状そうはなっていません。 二度の作り直し 実は上記は二度目のチャレンジでした。 一度目は Redshift(AWS) , 二度目が Azure SQL Data Warehouse(Azure) へとそれぞれの環境に構築しました。 その環境を利用して実際に分析業務を行っているメンバーに検証してもらい、その結果を受けてさまざまなディスカッションをした結果、作り直し、という意思決定を行っています。 なぜ作り直しという意思決定に至ったのか いろいろな理由はありますが、最大の要因は実際に分析業務を行うメンバーとのコミュニケーション不足だと考えています。 他社の事例などを参考に構築に関してはエンジニア側で主導しました。自分たちとしてはこうあるべきという絵図を描き、その形をきちんと実現しました。 もちろん構築の途中で分析業務を行っているメンバーにも、こういう構成で作りますという説明は行っていますが、その時点の相互理解が不足していたと考えています。結果として、運用不可を下げるというエンジニアの課題を解決することがメインになってしまい、分析業務を行っているメンバーの求めている形にはなっていませんでした。 それでも二度の構築に価値がなかったとは思っていません。実際にその基盤上で分析業務用のSQLを試してみてもらい、求めているものはこうじゃない、遅い、等フィードバックを通じて相互理解が深められました。 最終的な構成 AWS上にRDSを利用して構築しました。DBのエンジンはSQLServerを利用しています。 ついに完成しました。三度目の正直です。やったー どうしてこの構成になったのか 分析業務に関わるメンバー全員にとってはこの構成が最適だと判断しました。 データ分析を行っているメンバーは今まで通り分析業務が行えます。社内にあったデータ分析基盤はSQLServerがメインになっているものであり、さまざまな業務がSQLServerに最適化されている為です。既存の資産(分析用のクエリ等)が再利用できたり、データ分析業務を行っているメンバーの道具を変える必要が無かったり(例えばSQLServerManagementStudio 等がそのまま流用可能)、さまざまな利点がありました。 また、マネジードな製品を利用することでエンジニアの運用負荷も下げられます。課題としてパフォーマンスに対する懸念はありましたが、列ストアインデックスなどを利用してさまざまなパフォーマンスチューニングを行った結果、現行と同等の性能は出ています。 運用開始 社内のデータ分析基盤を利用していた業務は徐々にクラウドデータ分析基盤に移行していっています。 また、日々のETLの結果も下記のような形でSlackに投稿されています。 今後は安定的にこの基盤を運用していければ良いと考えています。 明日は id:aitam による オフサイト・開発合宿に。プロジェクター+ Wi-Fi環境のある宿3選 です。
アバター
この記事は 一休.com アドベントカレンダー 2017 の 18日目です。 一休.comレストランでアプリのUI/UXデザイナーをしている id:vivashion です。 私は一休.comレストランのアプリを開発し始めた時からデザイナーとして関わっています。 当アプリはファーストローンチから4度のメジャーアップデートをし、現在(Ver.5.3.2)に至ります。 ファーストローンチのVer.1.0.0、Ver.2.0.0、Ver.3.0.0のメジャーアップデートは大きくデザインが変わり、アプリ開発体制や環境も大きく変化してきました。 これまでのアプリ開発において紆余曲折ありましたが、その中で私がデザイナーとして何を考え、何を学んだか、ご紹介すると共に、この記事を読んでくださる皆様にとって良いプロダクト開発のための気づきやヒントになればと思います。 開発しているアプリはこちら。 クリスマスのレストランを簡単予約!一休.comレストラン IKYU Corporation フード/ドリンク 無料 一休.comレストランアプリ ローンチ Ver.1.0.0 アプリ開発プロジェクトの立ち上げ 一休.comレストランは、元々Webしかない状態でしたが、社内のエンジニアからの発案で、アプリを開発しようという意見が寄せられたことがアプリ開発プロジェクトが立ち上がったきっかけでした。しかし、「アプリ開発をしよう」と動き始めたはいいものの、社内にアプリ開発のノウハウが無かったのです。 そのような状況の中、たまたま弊社のエンジニアが、UIデザインを強みとしているGoodpatchの代表である土屋さんと知り合いで、アプリ開発におけるノウハウも多く持っていることから、アプリの共同開発をお願いしました。共同開発を行った理由としては、一緒にプロジェクトを進めながら自社内にもアプリ開発のノウハウを取り入れたいと考えていたためです。 Goodpatchからは、ディレクター1名、iOSデザイナー1名、 一休からは、iOSエンジニア1名、Androidエンジニア1名、Androidデザイナー1名(私)の 計5名でプロジェクトメンバーが構成されました。 また、GoodpatchからiOSエンジニア、Androidエンジニアの方が実装のアドバイザーとして協力してくださいました。 アプリ開発における全てのことが初めての経験で、全てのことが学びとなったプロジェクトでした。 Ver.1.0.0の開発の仕方 まずGoodpatchが一休.comレストランのサービスについて知ることから始まりました。どんなユーザー層がサービスを利用していて、どんなシーンで利用しているか。そして、どんなステップを踏んでレストランを予約しているか。それらの情報からペルソナを複数人作成し、ペルソナ毎のカスタマージャーニーを書き、リーン・キャンバスを埋め、ユーザーにどんな価値を提供していくかを詰めていきました。 この時、提供しようとした価値とは、「Webよりも簡単に予約できること」。例えば、Webよりも予約するまでの画面遷移のステップを減らすこと、予約するプランの見比べをしやすくすること等です。 このように開発するアプリの根底部分を固めた後、画面遷移図を作成し、プロトタイピングを行っていきました。まずは「色」の概念が入らないようにグレースケールで画面をデザインしていき、作って捨ててを何度も繰り返し、全画面を作成し、その後、サービスやペルソナに合わせたカラーを入れ込みアプリ全体のデザインを完成させました。 完成したデザインを基にエンジニアが実装していき、都度実装された画面を確認し、細かいデザインの調整も行いアプリをローンチすることができました。 Ver.1.0.0でデザイナーとして学んだこと 全てが学び アプリ開発に関するノウハウが皆無だったので、プロジェクトで発生するすべての事を学ぼうと必死でした。 プロジェクト発足当時、私はAndroidのデザイナーとしてジョインしましたが、それまでAndroidのデザインガイドラインも、iOSのHuman Interface Guidelines( HIG)もちゃんと見たことがない状態でした。アプリのデザインに関する知識や経験も皆無だったので、Prottを使ったプロトタイピング、デザインのフェーズにおいては、まずGoodpatchのiOSデザイナーの方が出してきた案に対し、それをAndroidのMaterial Designに落とし込むということを行い、アプリのデザインの仕方とMaterial Designについて学んでいました。 Android 4系とMaterial Designの壁 当時はMaterial Designのガイドラインもそれほど充実していなかったので、概念の理解を深めそれをデザインに落とし込み、ユーザーが意識しなくても使えるUIを心がけました。しかし、その当時はMaterial Designを実装するのに大きな壁がありました。Android 4系OSのシェアがまだ大きく、アプリのサポート対象としていたことです。Material Designを実装ベースで比較的簡単に表現できるのは5系のOSからでした。 Material Designの大きな特徴の1つとして、画面遷移のインタラクションになめらかなアニメーションを取り入れるというものがあります。いくらUIをMaterial Designにしても4系をサポートするとなるとこのなめらかなアニメーションを表現できないという制限があり、デザイナーとしては非常にもどかしい思いをしました。 今、Androidアプリをリデザインするとしたら、とてもスムーズでなめらかな画面遷移を表現でき、よりMaterial Designらしいアプリを開発できることでしょう。 (現在、Androidアプリの開発はほぼストップしていますが...) 1度目のメジャーアップデート Ver.2.0.0 Ver.2.0.0 開発プロジェクトの立ち上げ iOS、Android両アプリをファーストローンチして間もなく、会社の上層部からアプリのコンセプト変更とともに機能追加の要請が降りてきました。 降りてきた要望としては、「もっと簡単に操作できるデザインに」、「当日行けるレストランを見つけやすく」というものでした。この1度目のメジャーアップデートプロジェクトが発足した時点で、Goodpatchとの契約は終了し、新たに社内からディレクター1名、iOSデザイナー1名がジョインし、完全内製 のチーム体制になりました。 多くの問題を抱えながらのプロジェクト立ち上げ、iOS Ver.2.0.0のリリースでしたが、非常に多くのことを学べたプロジェクトでした。 (プロジェクトの途中にAndroidの開発は一旦中止となりました) Ver.2.0.0の開発の仕方 改めて新しいコンセプトのアプリのリリースに向けて、どんなアプリにするかミーティングを重ねました。 「もっと簡単に操作できるデザインに」のコンセプトに対しては、デザイン的なアプローチで改善する。「当日行けるレストランを見つけやすく」のコンセプトに対しては、地図検索で現在地付近のレストランを探せる機能を提供することに決定しました。さらに、新しくジョインしたiOSデザイナーからの提案として「ラグジュアリー感の演出」を追加のコンセプトとしました。 この時のプロジェクトでは、iOSデザイナーが画面遷移やデザインをガリガリ作って、それをエンジニアが実装していくスタイルでした。私が担当していたAndroidのデザインは、この時もiOSデザイナーが出してきたデザインをMaterial Designに落とし込むようにしていました。 Ver.2.0.0でデザイナーとして学んだこと チーム内コミュニケーションの大切さ 上述しましたが、私は、Ver.2.0.0の開発においてもiOSデザイナーが出してきたデザインをMaterial Designに落とし込むというアプローチを取っていました。 iOSデザイナーがデザインをガリガリ作って、どんどん作業を先行してしまったので、デザインの意図がどのようなものかというコミュニケーションが欠落しがちで、デザインの意図をよく理解しないまま、iOSからAndroidへのデザインの移植をしていました。 iOSデザイナーが1人で完璧なデザインを作れるということではないので、コミュニケーション不足によって改善できるポイントを見逃していた可能性もあります。より良いユーザー体験を提供するにはまずチーム内のコミュニケーションを透明にすることが大切だと改めて感じました。 自分の意見をはっきりと伝えることの大切さ Ver.2.0.0からジョインしてきたiOSデザイナーはそれまでアプリのデザインをしたことはありませんでしたが、優れたWebデザイナーでした。その彼がデザインした画面はHIGに沿っておらず、初めて目にするようなUIや、ユーザーが予期しないであろうインタラクションが散見されました。当然、ユーザーにとって使いやすいだろうとデザインされたものだったのですが、馴染みのないUIだったためにユーザーを迷わせていた箇所も多かったことでしょう。 私は、そのようなUIの欠点を認識していたにも関わらず、修正・改善すべきという発言を控えてしまっていました。当の彼が優れたWebデザイナーで、彼にデザインを任せておけば大丈夫というフィルターがかかってしまっていたためです。 結局、そのデザインのままアプリをリリースすることになり、ユーザーに提供してしまったことを後悔しています。自分が気づいていたUIの欠点、修正したほうが良いという意見をはっきりとチームメンバーに伝えていればそのような結果にはならなかったことでしょう。 アプリは会社の事業としての1つのプロダクトであり、収益を上げていかなければならない 至極当然のことなのですが、事業会社において、アプリはビジネスをしていく上でのひとつの武器です。つまり、アプリを開発したということはそれ相応の結果を求められますし、収益を上げられないアプリに価値はありません。 プロジェクトが立ち上げ時に会社上層部から提案された「当日行けるレストランを見つけやすく」というコンセプトには、それをコンセプトに掲げるだけの数字的根拠があり、ユーザーにアプリならではの機能を提供し、それの収益化が見込めるという判断によるものでした。 2度目のメジャーアップデート Ver.3.0.0 社内の組織体制の変更によりVer.2.0.0のiOSデザイナーが部署異動になり、晴れて私がアプリのメインデザイナーになりました。 CTO伊藤直也さんが一休にジョインし、アプリチームの開発体制、デザインの改善に乗り出しました。まず、直也さんからアプリのUIがどんなものであるべきか勉強会が開かれました。 この勉強会が大きなきっかけとなり、まずはHIGにできるだけ沿った形のUIにし、使い勝手を向上させることが大きな目的となりました。 Ver.3.0.0の開発の仕方 まずHIGからかけ離れている画面、UIを洗い出しました。例えば、アプリ立ち上げ後のトップ画面はドロワーメニューを採用していたり、検索のパーツが独自UIになっていたり、ナビゲーションバーが無い画面があったり、プッシュで画面遷移するべきところがモーダルで表現されていたり。それらの画面をそれぞれHIGに沿った形のUIにするためにプロトタイピングを繰り返し、開発メンバー全員で議論しながらアプリ全体を再設計しました。 Ver.3.0.0でデザイナーとして学んだこと HIGの大切さ HIGはiOSアプリをユーザーが触る上で非常に重要なものです。HIGに記載されているUIは、iPhoneユーザーが日常で頻繁に触れ、自然と使い方を学んでいて、意識せずとも操作できるUIであるからです。 そもそも一休.comレストランのアプリは、簡単に、スムーズにレストランを予約できるということが大前提です。なので、ユーザーがこの目的を達成するために、如何に意識させないUX、UIにするかを考えなければなりません。 プロトタイピングの大切さ プロトタイピングすることで、実装する前にアプリを疑似体験することができます。デザインモックをベースにユーザー体験を向上させるために議論し、プロトタイピングすることが実際のユーザー体験の向上につながります。ビジュアル化されたものはチームメンバーのさらなる改善案を引き出し、より良いプロダクトをデザインできます。 おわりに 今回は、ファーストローンチから、デザイン的に大きく変更がなされた2度のメジャーアップデートを通して学んだことを紹介しました。これらはデザイナーとして多くの学びを得た体験でした。 まだまだ一休.comレストラン アプリは、ユーザーにより良い体験を提供するために改善しなければならないことがたくさんあります。一休として「こころに贅沢を」を提供できるアプリになるよう、さらなる改善をしていきます。 明日はhayatoiseさんの 「Google Analytics APIでWordPressのタグとカテゴリのPV数を取得する方法」です。 qiita.com
アバター
この記事は 一休.comアドベントカレンダー2017 の17日目です。 残すところ一週間とすこしですね 一休.com スパ を運用・開発している id:kichion0526 です。 テクニカルな話や一休の苦労話etcは諸先輩方がたくさん書いてくれているので 最近、何を意識して実装しているかを書き残したいと… ギークなテッキーになりたかった故に小難しいことをしていた 前職からC#で書くことも多く、リフレクションなどを使い倒しメタプログラミングがすらすらできるようになるのが当時の目標でした 一例として ~ゲームにおける様々なアイテムを生成するファクトリ~ public abstract class ItemBase { protected ItemBase( int id) { Id = id } /// アイテムカテゴリを取得します。 public abstract ItemCategory Category { get; } /// アイテムIDを取得します。 public int Id { get; } /// アイテム名を取得します。 public abstract string Name { get; } } public enum ItemCategory { Weapon = 0 , // みんな大好き武器 Armor = 1 , // みんな大好き防具 GachaTicket = 2 , // みんな大好(ry ...(などなどいっぱい)... } public class Weapon : ItemBase { public override ItemCategory Category => ItemCategory.Weapon; ....(コンストラクタ等でid指定etc)... } public class Armor : ItemBase { public override ItemCategory Category => ItemCategory.Armor ; ....(コンストラクタ等でid指定etc)... } public class GachaTicket : ItemBase { public override ItemCategory Category => ItemCategory.GachaTicket; ....(コンストラクタ等でid指定etc)... } アイテムクラスを定義したら端的にswitch文でファクトリ作ると下記のイメージ public static class ItemFactory { public static T Create<T>( int id, ItemCategory category) where T : ItemBase { switch (category) { case ItemCategory.Weapon: return new Weapon(id); case ItemCategory.Armor: return new Armor(id); case ItemCategory.GachaTicket: return new GachaTicket(id); ...(その他もろもろ)... } } } 「新しいアイテムが追加されるたびにここをいじるのはめんどくさいなぁ」 と思うとリフレクションの出番です public static class ItemFactory { private static readonly Dictionary<ItemCategory, Func< int , ItemBase>> Items; static ItemFactory() { var constructors = typeof (ItemBase).Assembly.GetTypes() .Where(x => x.IsSubclassOf( typeof (ItemBase))) .Where(x => !x.IsAbstract) .Select(x => { // コンストラクタの引数の型 var argumentType = typeof ( int ); // コンストラクタ var constructor = x.GetConstructor( BindingFlags.Instance | BindingFlags.Public, null , CallingConventions.HasThis, new [] {argumentType}, new ParameterModifier[ 0 ] ); if (constructor == null ) return null ; // コンストラクタの引数 var id = Expression.Parameter(argumentType, "id" ); // コンストラクタをデリゲート化 return Expression.Lambda<Func< int , ItemBase>>( Expression.New(constructor, id), id ).Compile(); }) .Where(x => x != null ) .ToArray(); Items = constructors.ToDictionary(x => x( 0 ).Category); // Keyを生成するためなので引数は何でもいい } public static ItemBase Create(ItemCategory category, int id) { return Items[category](id); } } これで ItemBase を継承するクラスのコンストラクタを辞書化しておけるので Create メソッドがこんなシンプルに! しかも、辞書はstatic変数で保持しているのでアイテム生成のコストも気にしなくていい! (こんな書き方できるのかっこいい!) 何が得られたの? メリット 新しいカテゴリのアイテムが追加されるたびにファクトリを修正しなくて良くなった 生成メソッドの行数の少なさ (かっこいいという満足感) デメリット switch文の実装より初期実装に時間がかかる リフレクション知っているとしても初見は黒魔術 Dictionary なので2つ以上のクラスで同じカテゴリを使うと一律エラーで死ぬ アイテム種類数が少ないと単純に行数が増えてる 後任の人が困る材料になりかねない もともとは 「新しいアイテムが追加されるたびにここをいじるのはめんどくさいなぁ」 というモチベーションから始まった改修でした 今でこそ振り返ると 複雑な概念を導入した割には解決したことが薄すぎる と思っています こうなるとswitch文を書き換えないという選択肢の方が良さそうです 結局何が言いたいの? ホントの目的がないとテクニカルなコーディングはただの自己満足になると感じています 私自身、本当にテクニカルな手法が必要なのかを意識するきっかけになったのは一休に転職するようになってからです 極端な例では「この改修でどれくらい儲かるか」「今のチームの人が全員入れ替わったら扱えるか」なんてことを考えたりします 一休はエンジニアでもビジネス視点が求められているのでより「事業目標」に向かってコーディングできていると思います より目的を意識してプログラミングと付き合うと どうコーディングしたらいいか? の問いに答えが出しやすいのかなと実感してます まとめ 目的を見失ったプログラミングは自己満足になりがち という話でした この考えに至ったのも一休にいる優秀な方々のおかげだと思っています これからも社内のエンジニアの方から学びを得て全能感を高めていけたらと… つらつらと書きましたが先人たちがいい言葉を残してくれていますので、それで締めたいと思います プログラマが学ぶべき最も大切な技能というのは、コードを書かないときを知ることなのかもしれない。 最も読みやすいコードは、何も書かれていないコードだ。 (「リーダブルコード」和訳版 第13章「短いコードを書く」P.168 より) 明日は sagisakat さんの 「 アプリのローンチと2度のメジャーアップデート、何を考えてデザインしたか 」 です
アバター
この記事は 一休.com アドベントカレンダー 2017 の 15日目です。 一休レストランiOSアプリ の開発ディレクターをしています、 id:tsuchidah です。 クリスマスまであと10日となりました。 今回は、ふとした思いつきでアプリのアイコンをクリスマス仕様に変更して、どんなことが起きたのか、数字的な面や得られた知見などをご紹介したいと思います。 きっかけ 一休.comレストラン にとってクリスマスはとても大事な時期です。クリスマスに大事な人と素敵なひとときを過ごすため、レストラン選びは外せない…。多くのユーザーさんが一休.comレストランに訪れます。ですが、他の予約サービスなどを使うユーザーさんもたくさんいるはず。そこで、少しでも弊社サービスを候補に入れてもらおう、と考えた時にアプリをクリスマスアイコンにすることを決めました。 狙う効果は2点 Storeで目を引くこと → 新規インストール数の増加 ホーム画面で目を引くこと → 起動率の増加、そこからのコンバージョン クリスマスを意識していない人に早いうちからアイコンを目にしてもらい、「あ、そういえばそろそろか」「今年は一休で探してみようかな」と意識してもらえればいいな、という割りと軽い気持ちでスタートしました。 リリース 11月初旬に次のようなかたちでリリース。 背景画像に雪を降らせる…といった案もありましたが、11月上旬という時期は逃したくなく、このかたちに落ち着きました。クリスマスまで一月半ほど期間がありますが、実はクリスマス予約は10月初頭から始まっています。ハロウィンが終わり、11月は一般的にもクリスマスに向かって盛り上がっていく時期。いいタイミングでリリースできたのではないかと思っています。 結果 数字的な変化 新規インストール数 特に伸びませんでした。 これはサービス内容によりけりで結果は異なると思います。弊社サービスはレストラン予約サービス。レストランを予約する時、「まずStoreへ…」となる方は少ないと思います。とは言え、インストール数が増える時期なので多少拍車を掛けられるかもと期待をしたのですが、残念な結果となりました。 起動率 起動セッション数、DAUが伸びました! セッション数、DAUどちらもおよそ+20%ほど。アプリをフォルダーに入れる方も多く、なかなか目に触れないのでは…と懸念していたので、予想以上の数字でした。 こういった数字の成果が出たことは大変よい知見になりました。 予約数 明確な成果はわかっていません…。 予約数は上昇したものの、この時期は予約数が増える時期なので、クリスマス仕様にした成果とは断言できません。ただしセッションが増えた分CVRが下がらなかったので、悪い結果ではないと判断しています。 その他 別チームへの影響 効果を社内に共有したところ、PCブラウザ、スマホブラウザでも取り入れようという話になり、間もなくリリースされました。 クリスマスの日付をクリックすることで、すぐに検索結果に遷移することが出来ます。そのアイデアは思いつかなかったので、やられた!という気持ちになりました(笑) 社内からの声 とてもポジティブな意見をたくさんいただきました。また、「友達からいいね!と言われたよ」など、間接的にユーザーさんのフィードバックを受け取ることができました。アイコンやUIを季節に合わせて変えるという一見効果がわかりにくい施策で、ユーザーさんにポジティブな印象をもっていただけると思うと、やってよかったと心底思います。 まとめ ふとした思いつきだった割には、得られたものが多かったため、とても満足しています(笑)また、他社のレストラン予約アプリがクリスマスアイコンに変わっているのを見つけては、「弊社サービスが影響しているんだったらいいなぁ」と妄想をしています。 次もアイコンを変えるチャンスがあれば試してみよう、とチーム内で話していますので、次回乞うご期待です。 一点、iTunesConnectが12/22〜12/27までお休みのため、良いタイミングでアイコンを通常仕様に戻せるかが気がかりです…。 最後に、 クリスマスまであと少し、クリスマス仕様なのは今だけ!(笑)是非アプリやサイトに足を運んでみてください。 まだクリスマスのご予約がお済みでない方は、 クリスマス直前割 という企画も行っていますので、是非。 クリスマスのレストランを簡単予約!一休.comレストラン IKYU Corporation フード/ドリンク 無料 みなさま、素敵なクリスマスをお過ごしください。 明日は @hirosawak さんの「 一休.com iOSアプリでのfastlane使用例 」です。お楽しみに。
アバター
この記事は 一休.com アドベントカレンダー 2017 の 14 日目です。 昨日に引き続き、一休データサイエンス部の id:kitsuyui です。 13 日目のエントリでは Embulk, Redash, DatabaseMEMO の導入の経緯について解説しました。 とても素晴らしいツールを導入できましたが、実はそのままでは一休に導入することができない箇所がいくつもありました。 GitHub 上でどんなアクションをしたかを振り返りたいとおもいます。 その後、自分なりに見出したコントリビューションのコツと反省点について説明します。 私の英語力が不足していますので、つたない英語のプルリクエストを送ってしまっています。 この点についてはご容赦ください。 コントリビューション例 Re:dash + SQL Server が日本語が含まれていると正しく動作しない問題 Re:dash には Google のユーザ認証でログインする機能があるのですが、 このユーザ名と SQL Server の Query Runner がバッティングする問題がありました。 Re:dash から発行したクエリには SQL のコメントでユーザ名を埋め込むのですが、 そこが文字化けを引き起こしていました。 Re:dash のような Python 2.7 系ではありがちなこと (オープンソースソフトウェアの世界ではありがちなこと) ですが、文字コードを正しく扱っていないという基本的な点でした。 データソースの設定で文字コードを指定出来るようにし、クエリもエンコードするようにしてプルリクエストを出し、マージしていただきました。 https://github.com/getredash/redash/pull/1104 Re:dash の Azure SQL Data Warehouse 対応 Azure SQL Data Warehouse という Microsoft 公式の SQL Server 互換の DWH 向けソリューションがあるのですが、 こちらが Re:dash に対応していない、という問題がありました。 そもそも Re:dash は SQL Server に対して pymssql (+ FreeTDS ) を利用したクエリランナーを持っていたのですが、 こちらが Azure SQL Data Warehouse には対応していませんでした。 Python を使用して Azure SQL Database に照会する の解説にもある通り、 Microsoft としては pyodbc での実行ができることは検証されているため、 こちらを利用してアクセスできるようにし、プルリクエストを出し、マージしていただきました。 https://github.com/getredash/redash/pull/1906 embulk-output-jdbc で SQL Server Native Client (BCP) 対応 embulk は embulk-output-jdbc というプラグインをインストールすることで、各種の JDBC 互換のデータソースを出力先に追加することができます。 この embulk-output-jdbc には embulk-output-sqlserver が含まれています。 embulk-output-sqlserver には Windows 独自の機能として、 Native Client という Windows の DLL を利用した (非 JDBC) ロード方法があります。 これが SQL Server の最速の Bulk Insert 機能となっていますが、前述の通り Windows にしか対応していないという欠点がありました。 しかし、近年 Microsoft は Linux, macOS 環境にも力をいれており、 公式で ODBC ドライバを配布しています 。 こちらの ODBC のライブラリの中に Native Client (BCP) 周りの実装も含まれているため、 Linux, macOS でも実行可能であることがわかりました。 embulk-output-sqlserver の Windows 専用となっていた箇所にメスを入れ、 Linux, macOS にも対応し、 プルリクエストを出し、マージしていただきました。 具体的には、 Windows で読んでいる sqlncli11.dll というライブラリの代わりに Linux や macOS では msodbcsql.so, msodbcsql.dylib を使うように書き換えました。 途中 Shift_JIS (CP932) を前提とした箇所があったのを UTF-8 に対応する際にミスをし、一時的にバグを追加してしまいました。 こちらもすぐに修正をプルリクエストし、マージしていただきました。現在ではどの OS でも高速なロードができるようになりました。 一休内では予約情報のトランザクションや会員の行動履歴といった件数の非常に多いデータをロードする際にこのモードを活用しています。 該当の箇所で 5 倍以上の高速化が実現できました。 https://github.com/embulk/embulk-output-jdbc/pull/209 https://github.com/embulk/embulk-output-jdbc/pull/214 embulk-input-bigquery のバージョン変化に伴うプルリクエスト embulk-input-bigquery という BigQuery を入力データソースとして使えるようにする embulk のプラグインがあるのですが、 こちらが依存しているライブラリ google-cloud-ruby が動作しなくなっていました。 バージョンアップによりインターフェースが変わり 、クエリ結果のカラム名などを渡すインターフェースが、文字列ではなく Ruby のシンボルに変化していたためです。 embulk-input-bigquery にもこの変更を加え、プルリクエストを出し、マージしていただきました。 https://github.com/medjed/embulk-input-bigquery/pull/7 DatabaseMEMO (dmemo) の SQL Server 対応について dmemo はテーブル情報を取得するデータソースとして、 MySQL, PostgreSQL, Redshift に対応しているのですが、 未だ SQL Server には対応していませんでした。 内部の実装をみたところ、テーブル情報を取得する際に、 Rails の Active Record を使っているようでした。 activerecord-sqlserver-adapter という、 SQL Server 用のアダプタを見つけたので、こちらを利用できるように実装しました。 なんとなく勘所はつかめましたので、 Active Record 用のアダプターさえあれば、ほかの種類のデータベースも移植できるかもしれないと考えています。一休ですと、他に BigQuery, Presto などが候補としてあります。 こちらは私 kitsuyui の方の実装がまだこなれていない点があるので、 GitHub 上でやりとりさせていただいている最中です。 https://github.com/hogelog/dmemo/pull/91 DatabaseMEMO (dmemo) を閲覧のみログインせずできるようにする DatabaseMEMO は Google のアカウントでログインできるのですが、ログインせずに社内からの閲覧のみは許可したいケースがありました。全員が Wiki の編集者とならずとも、利用はできるようにしたいのです。 こちらは crowi-plus などにも 「ログインしていないユーザーにも閲覧のみ許可するオプション」があるのと同様の意図です。 こちらも GitHub 上でやりとりさせていただいている最中です。 https://github.com/hogelog/dmemo/pull/93 まとめ SQL Server について 基本的には「SQL Server に対応してない」のケースは多いです。 しかし、実際には社会的な土台はほぼ整っていて、ソフトウェア的には少しの修正で対応できてしまう、というケースが多いです。 以前であれば、 SQL Server はエンタープライズ製品・導入が難しい・検証には Windows 機が必要、といういろいろな壁があったかと思います。 しかし、今ではありがたいことに SQL Server には Docker 版 が存在し、 Linux や macOS でも充分に検証できるようになりました。 SQL Server 自体はオープンソースソフトウェアではないですが、実行環境自体がオープンソースソフトウェアに近い構造で用意されていることで、 検証にかかるコストがグッと減りました。 この点は大きな転換点になると思います。 コントリビューションのコツ 期待以上の動作をさせてみる 私なりのコントリビューションのコツは、オープンソースソフトウェアを使い始めたときに、期待以上の使い方をすることです。 あえて Linux で embulk-output-sqlserver を Native Client で動作させようとしてみる あえて DatabaseMEMO (dmemo) を SQL Server で動かしてみる あえて Re:dash で Azure SQL Data Warehouse を使ってみる こういうときに、最初から期待以上の動作をすることはありません。しかし、そっと閉じるのではなく、 想像力を働かせて「似た環境では動いてるはずなのに、なぜ?」「誰かの環境では動いているのに、なぜ?」といった問いからスタートして、あとは地道に帳尻を合わせていくことです。 できそうな材料をしらべる ここまでわかれば、まずは一点だけが突破するような状態を作ることができます。 embulk-output-sqlserver で呼んでいる DLL の関数を調べ、全てが ODBC ドライバにもあるかを調べる DatabaseMEMO で使っているフレームワーク (Rails) のドライバを調べ、 SQL Server 版が使えないか調べる Re:dash の実装言語である Python で Azure SQL Data Warehouse に接続する方法を調べる 道具を揃える 材料があっても、見知らぬ環境ではうまく戦えません。道具を調べます。 ビルド方法・テスト方法・デバッグ方法・インタプリタ・パッケージマネージャあたりをおさえておけば大丈夫です。 embulk の場合は ./gradlew を叩けばビルドができます。 embulk irb でインタプリタに入れます。 DatabaseMEMO は Dockerfile があるので Docker で環境をつくれます。 rails console コマンドで Rails の irb に入れます。 Re:dash は (Python なので私はあまり困りませんでしたが) ./manage.py shell でインタプリタに入れます また、未知のオープンソースソフトウェアの場合だと、これらを調べるのに時間がかかってしまうことがあります。 その場合には strace (Linux のプロセスが実行するシステムコールを見ることができるコマンドです) netcat (ポートの開閉をしらべたり、ポートを転送したり、サーバをでっちあげたりするのに使います) print デバッグ (あまり上品ではありませんが、どこでも使えます) などの、より低レベルな道具を使えば問題ありません。 (どこでも使える、というメリットもあります。) また、利用するソフトウェアに CONTRIBUTION.md やライセンス等がある場合、それらも読み込んでおきます。 一点突破 後先考えずにあちこちに手を入れて、一点でいいので動くようにします。 このレベルでは 100% の状態である必要はありません。ボロボロでも疎通までができれば最高です。 DLL のパスだけ .so や .dylib に変えてコンパイルすると、 Linux でも文字化けした状態でなら文字列が挿入できる状態 SQL の実行の大部分は失敗するが、 DatabaseMEMO から SQL Server への接続まではできる状態 幾つかのデータベースが見えないが、 Re:dash で SELECT 1 クエリが実行できる状態 きれいにする 一点突破したら、あとはそこを中心に綺麗に整地していきます。 一点突破したときにソースコードを散らかしてしまった場合も、このタイミングできれいにしていきます。 なるべく git diff が最小になるようにします。場合によっては複数のプルリクエストに分けます。 ドライバのパスや名称を指定出来るようにする ・文字化けしないように修正 SQL Server 用にソースコードをちまちま修正 ( SELECT ... FROM ... LIMIT n を SELECT TOP n ... FROM ... に、といった翻訳) データベース一覧のクエリなどを修正する あとはコミッタの方と相談 ここまでできたら、プルリクエストが作れます。 当たり前ですがコミッタやメンテナのほうが、自身のソフトウェアに対して深い洞察とモチベーションをもってます。 できるさえわかったら、深追いする前に Issue などで挙げて、具体的な実装については相談しつつ進めるのが良いと思います。 反省点 自分の場合、原因がわかると修正も同時にできてしまうことが多いので、 Issue とプルリクエストを同時に送ってしまう、 またはプルリクエストのみを送ってしまうというということが多かったです。だいぶ失礼なことをしてしまったと今は反省しています。 また、 Issue でのディスカッションを踏まえて実装に入るほうが、より問題の本質を捉えやすかったかな?とも思います。 この点は 2018 年は改善していきたいです。 最後に 日頃いろいろなオープンソースソフトウェアにお世話になることが多いので、来年もどんどんオープンソースソフトウェアを利用し、ガンガン還元していきたいです。 明日 (15 日目) は tsuchidah さんによる「アプリアイコンをクリスマス仕様にしたらどうなった」です。
アバター
この記事は 一休.com アドベントカレンダー 2017 の 13 日目です。 一休データサイエンス部の id:kitsuyui です。データエンジニア兼データサイエンティストをやっています。 この記事はもともとアドベントカレンダー上では「脱・神 Excel (仮)」という名前で枠で取っていたのですが、 少し主語が大きすぎたかな?と反省しています。 書いているうちに全く主旨が変わってきましたので、副題とさせていただきました。 今回は一休社内でのデータエンジニアリングにまつわる負担、それらを解決する Redash, Embulk, DatabaseMEMO の導入の流れを書こうと思います。 また、その過程で副次的に発生した FLOSS へのコントリビューションなどなどについては、 14 日目のエントリで説明したいと思います。 一休とデータ活用 一休は今日まで上質な宿・レストランの予約サービスを運営してきて、今年の 7 月で創業 20 年目になりました。 Web でのサービスを提供する企業としては比較的に古参プレーヤーの方だと思います。会員数も 500 万会員を突破しています。 蓄積されてきた膨大なユーザの行動・予約データがあり、データの持つ価値が非常に大きい企業です。 2012 年ごろから、一休ではデータ分析を重視して施策を決定することが増えてきました。 2014 年ごろから、 BigQuery などの巨大データ向けのソリューションを併用することが増えてきました。 2015 年ごろから、基幹データベースとは独立したデータ分析のための専用のデータベースとして、データウェアハウスの構築が進んできました。 今では一休社内の様々な施策が、データドリブンまたはデータ分析にもとづいて行われるようになっています。 1. Re:dash 導入によるデータの民主化 Before 社員にデータ分析の習慣が定着していき、こぞってデータを見るようになると、データの抽出業務が大量に発生していました。 エンジニアでなくとも CSV ファイルさえあれば Excel で勝手に分析することができるのですが、その CSV ファイルこそがデータエンジニアなしでは用意できないものだったのです。 データ活用者からすると分析をしたいのにデータが手に入るまで時間がかかり、データエンジニアからすると「自分は右から左にデータを渡すだけで手一杯になってしまう」というところに負担やフラストレーションを感じるという、お互いに嬉しくない状態になっていました。 Do そこで一休では 2016 年にデータウェアハウスと共に Re:dash を社内に導入しました。 Re:dash はデータベースへの接続機能を持っており、登録した SQL を実行することで、きれいなグラフや表を生成することができます。 また CSV や Excel ファイルを生成することができます。 Re:dash は Web アプリケーションであり URL を持つので、 URL を Slack で共有することができます 。 After 定型的なデータ抽出作業は全て Re:dash のボタンを押すだけで実行できるようになったため、データエンジニアがボトルネックとなることは減っていきました。 また、一休では Re:dash 以外にも Tableau を営業ツールとして導入しており、これもデータエンジニアの負荷を低減しています。 2. データウェアハウスと Embulk Before Re:dash によってデータ抽出業務の負荷は大きく下がったのですが、データを観る人が増えたことによって、ますます社内のデータ活用のモチベーションは上がりました。 そのため、基幹データベースからデータウェアハウスにロードしたり、その過程で正規化するタスクしたり、また別のデータベースに移し替えたりという、いわゆる ETL 業務の効率化と安定性が急務になっていました。 しかしながら、個々の ETL のコンフィギュレーションが分散して存在していたため、著しく可搬性・メンテナンス性が損なわれていました。 Do そこで Embulk の出番です。 embulk では .yml ファイルで ETL (Extract, Transform, Load) の組を記述することができます。 in: type: sqlserver table: some_table ... out: type: sqlserver table: some_table ... 素晴らしい特徴として、 input と output のインターフェースが完全に独立しているということがあります。 そのため、「データウェアハウスに入れているあのテーブルを BigQuery にもロードしたい」というようなケースでは、単に output だけ変えればそのまま動作します。 After embulk の導入によって、いろいろなサーバに分散していた ETL 処理とそのコンフィギュレーションを 1 つのシステムとして統合することができました。 また、これらの .yml ファイルは GitHub で管理しているため、 ETL の過程がどのように変化したかを時系列で追うことができます。 3. 肥大化するテーブル定義の山と DatabaseMEMO の導入 社内でデータ分析がいよいよ活性化して、道具が整ってくると、今度はデータの文脈や名称に対しての理解が重要になってきます。 今日まで何度も改良を加えながら受け継いできた基幹のデータベースは 宿泊予約 のサービスで 500 テーブル以上 、 レストラン予約のサービス で 200 テーブル以上 、その他全社の基幹システムのテーブルは合計で 1,000 テーブルを超え ます。 また、基幹システム以外にもデータ分析用に使うテーブル ( 500 テーブル以上 ) や、社内システム用のテーブル定義なども含めると、 2,000 テーブル以上 にもなります。 カラム数はさらにその 10 倍程度 でしょうか。 Before これら基幹データベースのテーブル定義は、全て SVN 上の Excel ファイルに記述する運用をしていました。 2016 年に SVN から GitHub への切り替えを行ったのですが、 Excel によるテーブル定義は Git での差分検出・マージがしづらい という問題がありました。 また、 Excel ファイル自体の行数・列数が大きくなりすぎ、検索性が下がり開発速度が非常に低下 していました。 この Excel ファイルが含んでいた職人的なマクロはメンテナンス不可能になっていました。 一方で Redash の導入により 営業やマーケターの中にも SQL を書ける方が増え てきました。 しかし、基幹 DB の定義は GitHub に置かれていたため、正しい定義はエンジニアにしか公開されていません でした。 また、 データウェアハウスのテーブル定義がどうなっているのかは、そのテーブルを作った本人以外には誰にもわかりません でした。 Do そこで DatabaseMEMO (dmemo) を導入しました。 DatabaseMEMO は Cookpad がつくった データベース・テーブル・カラムを Markdown 書式で記述・検索・閲覧できるデータベース専用の Wiki のようなものです。 サーバを設置し、簡単なスクリプトを書いて旧来の Excel によるテーブルの定義をインポートしました。 また、 一休独自のカスタマイズとして、テーブル説明文から GitHub へリンクも付与し、ログインなしでも閲覧までは可能に しました。 After 旧来のテーブル定義の歴史を維持しながらも、より高速に検索したり、気軽に Tips を追記したり ということができるようになりました。 テーブルが ソースコードのどこで使われているかも、瞬時に検索 することができます。 さきほど挙げた Re:dash のメリットと同じですが、 DatabaseMEMO は URL を持つので Slack 上で定義を簡単に受け渡しすることができるようになりました 。 まとめ このようにして、データ分析業務に関わる様々なタスクの効率化・高速化・明文化を推し進めています。 世界的にデータ活用が叫ばれている時代の中で、データエンジニアは単にデータ活用の召し使いになるのではなく、だれもがデータ活用できるような風土づくりを率先して行い、社内のデータ活用のハードルを引き下げていく事が重要だと、私は考えています。
アバター
この記事は 一休.comアドベントカレンダー2017 の 8 日目です。 一休.com の宿泊開発基盤のお手伝いをしている id:shiba-yan です。 はてなインターン時代の縁で naoya さんから声をかけていただき、基本フリーランスですが一休で週に 3 日ほどの作業を 2016 年 4 月から行っています。 最近は shibayan とも一緒に改善を進めている 4ヶ月の間に一休.comで起きた変化 - zimathon blog 2016 年 4 月末から現在までに、一休社内でどのようなことに取り組んできたか、公開できる範囲で思うままに書いていきます。長いです。 ユニットテスト基盤 新しいメールテンプレート メール配信基盤 宿泊クラウド移行 移行方法の調査・検証 実行環境の調査・検証 アプリケーションの分離と整理 AppVeyor での CI / CD 本番環境の移行 その後の運用 宿泊アーキテクチャの改善 終わりに ユニットテスト基盤 これまではテストが書ける状態ではなかったのですが、xUnit.net を利用してユニットテストをコンポーネント単位で書けるような仕組みを用意しました。 しかし、残念ながら上手く回っていません。例外的ですが、複雑化した URL Rewrite ルールに対するユニットテストに関しては、上手く回すことができています。チームメンバーの id:minato128 が LT で話したスライドもあります。 URL Rewrite のテストは勢いでライブラリを作り、それをすぐに実践投入しました。クラウド移行のタイミングではこのテストのおかげで不具合が発覚し、非常に助けられています。 新しいメールテンプレート 長く開発されてきた結果、一つのシステム内に複数のメールテンプレートが実装されていて、それがアプリケーション内に分散している状態でした。 さらに条件によって項目が変わるメールは非常に複雑なテンプレートとして定義されていて、メンテナンスが非常に難しい状態になっていたため、ASP.NET MVC で使われている Razor ベースの新しいテンプレートを作成しました。 しかし、残念ながら利用は広がっていません。既にあるテンプレートを移行することを考えると非常に高コストになり、最近では新しくメールを増やすといったことがほぼないのが理由でした。このあたりはヒアリング不足で作業を進めてしまったのが反省点です。 メール配信基盤 これまで一休ではオンプレミスのメールサーバーを利用して来ましたが、一斉メール送信に時間がかかる問題が顕著になり、さらに保守にかかる手間とコストやクラウド移行という課題が出てきたため、AWS 上に新しく実装することになりました。 実装したメール配信基盤の概要は以下のようになります。 SendGrid を利用 SQS と Elastic Beanstalk を使った非同期処理 メールの配信結果は DynamoDB に格納 Webhook は API Gateway と Lambda で処理 ASP.NET Core を利用して実装 去年から段階的に一休.com のサービスで利用を始め、今ではほぼ全てのメールが AWS 上のメール配信基盤から送信されるようになっています。メール配信基盤に関しても id:minato128 がセッションを行っているので、そちらも参考にしてください。 今回のアドベントカレンダーでも書かれているので、こちらもどうぞ。 初期は多少 DynamoDB の予約スループットを使い切ってしまうことがありましたが、実装の改善により今では全く発生しなくなっています。SendGrid の障害が一時頻繁に発生した時には悩まされましたが、現在は対策を行い安定した運用を行えています。 宿泊クラウド移行 直近の大きな作業として、一休.com サービス全体のクラウド移行がありました。既に一休.com のサービスは全て AWS に移行が完了しているので、皆さんが見ている www.ikyu.com は AWS の東京リージョンから提供されています。 AWS への移行を行うことでサービス自体の可用性が高まったり、柔軟なスケーリングが可能になったりとクラウドのメリットを享受することが出来ていますが、移行完了までには様々な問題が山積みでした。 単純に宿泊のアプリケーションと言っても、正しく動かすためには関係するアプリケーションも同時に移行する必要があります。例えば、実際に宿泊のアプリケーションでは 4 つのアプリケーションを移行する必要がありました。 宿泊サイト本体(www.ikyu.com) マイページ(my.ikyu.com) 管理画面 外部連携用 API その他にも内部から利用している API が複数あり、宿泊アプリケーションが AWS へ移行するためには、それらを先に AWS へ持っていく必要があったのです。他にもいろいろあり、担当している宿泊基盤チームだけで合計 8 アプリケーションを AWS に移行することになりました。 いきなり巨大な宿泊サイトから行うのは非効率なので、まずは規模の小さいアプリケーションから地道にノウハウを溜めつつ進めることにしました。 移行方法の調査・検証 まずはオンプレミスで動作しているアプリケーションを、どのように AWS 上で実行するかを検討しました。一番単純かつ時間がかからないのはオンプレと同じ構成を EC2 で作成して、今と同じアプリケーションをデプロイする方法でしたが、開発の現場では日々デプロイの問題に悩まされていて、今の状態のまま AWS に持って行っても悪くなるだけなのは、火を見るよりも明らかでした。 当時のデプロイでの辛さは、これもまた id:minato128 が話したスライドがあります。 デプロイ完全自動化から1年で起きたこと /ikyu-deploy // Speaker Deck さらに当時採用していた Jenkins を利用したデプロイは、リポジトリ内の変更されたファイルのみを対象としていたため、EC2 に持って行った場合にインスタンスごとにファイルの整合性を担保することが非常に難しいことが分かりました。 その他にもたくさんの課題がありましたが、調査と検証を重ねていった結果として以下のように方針を決定しました。 オンプレと同じ仕組みは 一切持ち込まない EC2 インスタンスは何時でも破棄が出来るように 差分ではなくアプリケーションに必要なファイル全てをデプロイする ビルドには Jenkins を止めて AppVeyor を利用する 一般的な方法でアプリケーションのビルドが行えるように構造を変える 実際に実行する EC2 インスタンスは Elastic Beanstalk を利用して、インスタンスの生成と破棄やデプロイなどを全て任せることにしました。最初は多少デプロイが非効率になったとしても、まずはオンプレ構成からの脱却が重要だと考えました。 実行環境の調査・検証 アプリケーションは Elastic Beanstalk を使って実行する方向に舵を切りましたが、宿泊アプリケーションでは一部に特殊なコンポーネントを利用しているため、事前に開発環境でデプロイ用のパッケージをビルドして、問題なく動作が可能かを検証しました。 当初は ebextensions を利用してインストールを試しましたが、Cloud Formation の実行権限が特殊で上手くいかなかったため、カスタム AMI を作成して運用する方向に決めました。 Windows Server 2012 R2 を利用(当時は 2016 がリリースされていなかったため) CircleCI を利用して AMI を自動で生成 Windows Update は新しい AMI を作成して対応 出来るだけ手動で行う部分を減らして、運用時の負荷を下げるように工夫しました。CircleCI を使った新しい AMI のビルドは 20 分以内で完了するので、Beanstalk に設定すれば EC2 が自動的に作り直されて作業は完了します。 今回はカスタム AMI を使わざるを得ない状況でしたが、結果的にプロビジョニング時間の短縮に繋がりました。 アプリケーションの分離と整理 一休.com のアプリケーションは ASP.NET で実装されていますが、歴史的経緯から先ほど述べた 4 つのアプリケーションが 1 つのアプリケーションとしてマージされた形で実装されていました。しかし、プロジェクトファイルは別々に存在していたので、このままでは MSBuild を使ってデプロイ用の資材をビルドすることも難しい状態でした。 まずは一つになっているアプリケーションを、役割ごとに分離する作業は必須だと考えました。実際のアプリケーション単位で分離することができれば、後は普通の ASP.NET アプリケーションと同様の方法が使えるからです。 当時は分離が本当に必要なのかという意見が何回か出てきましたが、移行を担当したチーム内では必要な作業だという認識があり、理由の周知を行い対応しました。 まずは Qiita に DesignDoc を作成してフィードバックを貰い、内容に問題が無ければ周知用のエントリとして仕上げるという作業を繰り返し行いました。 実際の作業を行ったのは私なので、その時に方向性を以下のように定めました。要するに普通の ASP.NET アプリケーションに組み替える作業です。 1 つの巨大なアプリケーションを 4 つに分ける 適切な粒度で分離 アプリケーション毎にソリューションを用意してフルビルドが簡単に行えるように 参照がアセンブリ直指定になっていた部分をすべて修正 アプリケーションの一部であればプロジェクト参照 NuGet で配布されている場合はパッケージ参照 実際に開発が日々行われているリポジトリに対して、非常に大きな変更を行うことは開発チーム全体への影響が大きいため、事前のリハーサルを何度も行いビルドやデプロイに影響が出ないように努力しました。 どのくらいの規模だったかは、GitHub の Insights が物語ってくれています。 各アプリケーションを順番に分離していき、作業が完了するまでに 2,3 ヵ月が必要でした。しかしこの分離作業が完了すれば、クラウド移行に必要な作業の 9 割は完了すると考えていたので、あえて時間をかけて丁寧に作業しました。 AppVeyor での CI / CD アプリケーションの分離を行った結果、フルビルドやデプロイ用パッケージの作成は MSBuild を 1 回実行するだけで完了するようになり、大幅に処理をシンプルにすることが出来ました。 これまでの Jenkins を利用したデプロイでは、環境に依存する Web.config や App.config などの設定ファイルをスクリプトでコピーするような形になっていましたが、その方法を止めて全て Xml Document Transform を使ったビルド時の自動切り替えを利用するようにしました。 Web.config Transformation Syntax for Web Project Deployment Using Visual Studio よくある Web.Release.config などと同じ方法で、ステージングや本番といった単位で変換ファイルを用意しました。環境ごとにデプロイパッケージを作成する場合も、環境名 *1 を MSBuild のパラメータに指定するだけで済みます。 ここまでの変更で AppVeyor でも簡単にデプロイパッケージが作れるようになりました。 実際に今では 1 日に数多くのビルドを行い、Elastic Beanstalk へのデプロイを実行しています。 少しビルドとデプロイに時間がかかってしまっているのが課題としてありますが、今後のアプリケーション改善によって短縮を見込んでいます。 本番環境の移行 本番は規模の小さいアプリケーションから順に移行していきましたが、メインとなる宿泊アプリケーションに関してはリクエストが多く、移行作業での障害が発生した場合には多大な影響が発生します。そのため、アプリケーションの移行準備が完了してからの 1 ヵ月ほどは動作検証と負荷テストに時間を割り当てました。 基本的な動作検証には、既に利用している Selenium ベースの E2E テストを利用して実行しました。他にも運用担当者にお願いをして、AWS 上のアプリケーションで普段と同じ業務を試してもらうといった方法で進めていきました。 負荷テストはインスタンスサイズの大きな EC2 を用意し、そこから JMeter を使って特に重要なページと API に関して負荷をかけ、必要なインスタンスサイズと台数を決定しました。この辺りはテストの鬼 id:akasakas がほぼやってくれました。 www.ikyu.com は AWS 移行の前に Fastly への全面的な移行を完了していたため、本番環境を AWS 上に用意してしまえば、後は Fastly 側でバックエンドを切り替えるだけで移行が完了します。 何か意図しない挙動が発生した場合には即座にオンプレに戻すと決めていましたが、そういったことは発生することなく、極めて平和に終わった本番移行でした。 その後の運用 準備と検証をしっかりと行ったため、クラウド移行は特に大きな問題もなく、スムーズにほぼ予定通りに完了することが出来ました。事前の負荷テストによって決定したサイジングもほぼ想定通りでした。 今回上げた内容以外にも、クラウド向けにアプリケーション側で最適な形となるように作業を行った結果、サービス提供に必要なマシン台数も大幅に減らしつつ、可用性を高めることが出来ています。ぱっと思いつくだけでも以下のようなメリットが、AWS への移行で得られました。 オートスケーリンググループによる柔軟なリソースの割り当て 不良インスタンスは自動的に破棄、再生成 Web サーバーのメンテナンスがほぼ不要に Datadog と New Relic のメトリックを見るぐらいに ホスト OS に対する更新をイミュータブルに実行 ローリングアップデートを自動で行い、ヘルスチェックが通らない場合は自動的に元に戻される デプロイが原因となる障害発生なし 半分は Elastic Beanstalk を利用したことによって得られています。一休社内では Elastic Beanstalk を多用していて、今 naoya さんが中心になって進めている、新レストランサービスでも Elastic Beanstalk と Docker が利用されています。 オンプレ時代にデプロイの課題を解決するために作られた Slack チャンネルがアーカイブされたのも、クラウド移行に伴ってデプロイに起因する問題が解消されたことの表れでもあります。 今回、移行には非常に長い時間がかかってしまいましたが、単純に移行するのではなく最適化した形で持って行ったことで、数多くの課題が同時に解決されたと考えています。 移行が完了して 1 ヵ月後に打ち上げを行い、クラウド移行での思い出を語り合いました。 内容と全く関係はありませんが、赤坂には良いレストランが数多くあり、 一休.comレストラン を使えば簡単に予約することができます。実際に打ち上げ会場は一休レストランで予約しました。 まだまだ積み残した課題は多いですが、なかなか経験できない重要な作業に参加することが出来て、非常に勉強になりましたし楽しかったです。 宿泊アーキテクチャの改善 元々は宿泊アプリケーションのアーキテクチャ改善のために誘われていたのですが、クラウド移行が完了したので最近になってようやく本格的に取り掛かれるようになりました。 クラウド移行のタイミングでプロジェクト間の依存関係を徹底的に整理した結果、Visual Studio と ReSharper を使った機械的な解析が行えるようになりました。現在は naoya さんからの助言もありコードリーディングを深くまで行い、問題点をしっかりと理解してまとめる作業をしています。 まだ始まったばかりですが、アーキテクチャを改善し開発効率だけではなく、それに伴ってパフォーマンスの向上までを目標としています。 終わりに 明日は juri-t さんによる「最近流行りのword2vecをLDAと比較してみた」です。 *1 : Production / Staging など
アバター
GoとSQL Server
この記事は、[一休.comアドベントカレンダー2017]の7日目です。 qiita.com こんにちは、データサイエンス部・大西 id:ohke です。 ユーザの行動収集基盤や、マーケティング施策の実行を支援するシステムの開発・メンテナンスを担当しています。 7日目の本投稿では、GoでSQL Serverを使う方法について、紹介したいと思います。 なぜGoとSQL Serverなのか メジャーじゃない組み合わせだと思いますので、なぜGoからSQL Serverを使うことになったのか、背景を補足します。 今年に入り、一休ではデータウェアハウス基盤をクラウド環境に構築しました。 この基盤では、リアルタイムな行動ログを含む、マーケティングに必要なデータを全てSQL Sever(Amazon RDS)に集約しています。 この基盤を使った施策の一貫として、ユーザのリアルタイムな行動を分析し、今一休に訪れているユーザへ1 to 1マーケティングを検討しています。 具体的には、サイトやアプリでのメッセージの通知などです。 ブラウザ、フロントエンドサーバ、アプリなど、様々なアプリケーションからアクセスされるため、Web APIで共通したインタフェースを提供するのがセオリーですが、今いるユーザを対象とした施策においてはPV数に比例したアクセスが予想されます。 こうした高負荷に耐えるための環境として、行動ログの収集でも実績があるGoをAPIサーバとして構えることになりました。 行動ログ収集の取り組みについて興味のある方は、こちらもご覧ください(ちなみに、当時はSQL Serverではなく、Azure SQL Data Warehouseをデータウェアハウスとして採用してました。変更された経緯については、 12/20投稿予定の「データ分析基盤、その後 id:sisijumi 」で触れます)。 2017-08-17-_DataAnalyticsPlatform.pdf // Speaker Deck また、可能な限りリアルタイムな行動ログを使う(目標タイムラグは1分以内)ため、データマートなどを介在させずに、Goで動くAPIサーバからSQL Serverに直接アクセスする必要があり、今回の調査に至りました。 生のSQLを実行する Goでは、 database/sql という標準ライブラリがSQL(-like)なインタフェースを提供しています。 DB製品ごとのドライバと組み合わせることで、DBへ直接SQLを実行できるようになります。 SQL Serverのドライバとして、 go-mssqldb と gofreetds の2つがあります。 go-mssqldbはGo単体で実装されていますが、gofreetdsはcgo(GoからCのコードをコンパイルしたり、CのライブラリをリンクできるようになるGoのビルド機能)を使ってます メジャーなのは、go-mssqldbのようです(GitHubスター比較) 今回はgo-mssqldbについて解説していきます。 https://github.com/denisenkom/go-mssqldb go-mssqldb パッケージをダウンロードしておきます。 > go get github.com/denisenkom/go-mssqldb まずは接続です。ポイントは3点です。 go-mssqldbをブランクインポート(_)して、SQL Serverのドライバで初期化します これでdatabase/sqlのインタフェースでSQL Serverにアクセスできるようになります 接続文字列は3パターンで記述できますが、パスワードに ; (セミコロン)を含む場合は2番目か3番目を選択する必要があります server=testdb.ikyu.jp;user id=ohke;password=p@ssw0rd;database=TestDB odbc:server=testdb.ikyu.jp;user id=ohke;password=p@ssw0rd;database=TestDB パスワードに ; を含む場合は、 password={p@ss;word} のように {} で括る sqlserver://ohke:p@ssw0rd@testdb.ikyu.jp?database=TestDB パスワードに ; を含む場合は、 p@ss%3Bw0rd のようにURLエンコードする sql.Open()の第1引数に"sqlserver"または"mssql"を指定して接続します 2つは基本的に同じですが、クエリパラメータの渡し方に違いがあります(後述) package main import ( "database/sql" _ "github.com/denisenkom/go-mssqldb" ) func main() { connectionString := "sqlserver://ohke:p@ssw0rd@testdb.ikyu.jp?database=TestDB" // 接続 // "sqlserver"の代わりに"mssql"でもOK connection, err := sql.Open( "sqlserver" , connectionString) if err != nil { return nil , err } // 切断 defer connection.Close() // CRUD処理を記述 // ... } 接続すれば、database/sqlのインタフェースに則って、SQLを実行できます。 まずはselectです。 1行のselectならQueryRow、複数行のselectならQueryと使い分けます ドライバに"sqlserver"を指定した場合、 @ で始まるパラメータ名をSQLに埋め込み、sql.NamedArg構造体でパラメータに渡す値を設定します "mssql"で指定した場合、 ?n をSQLに埋め込み、2つ目以降の引数で ?n に渡す値を設定します( ?1 ならば2番目の引数、 ?2 なら3番目の引数の値が渡されます) // select(1行) row := connection.QueryRow( ` select name, registered_at, valid from members where member_id = @member_id` , sql.NamedArg{Name: "member_id" , Value: 1 }) // ドライバに"mssql"を指定した場合 // row := connection.QueryRow(`select name, registered_at, valid from members where member_id = ?1`, 1) var name string var registeredAt time.Time var valid bool if err := row.Scan(&name, &registeredAt, &valid); err != nil { return } fmt.Println(name, registeredAt, valid) // select(複数行) rows, err := connection.Query( `select name from members` ) if err != nil { return } defer rows.Close() for rows.Next() { if err := rows.Scan(&name); err != nil { return } fmt.Println(name) } 続いて更新処理です。 更新(insert、update、delete)はExecメソッドで実行します Resultオブジェクトが返されます RowsAffected()で、処理された行数を取得できます LastInsertId()で、挿入時のidentityの主キー値が取得できます 設定されていない場合は-1 // insert if result, err := connection.Exec( ` insert into members (member_id, name, registered_at, valid) values (@member_id, @name, @registered_at, @valid)` , sql.NamedArg{Name: "member_id" , Value: 1 }, sql.NamedArg{Name: "name" , Value: "onishik" }, sql.NamedArg{Name: "registered_at" , Value: time.Now()}, sql.NamedArg{Name: "valid" , Value: true }); err == nil { insertedNumber, _ := result.RowsAffected() insertedId, _ := result.LastInsertId() fmt.Println(insertedNumber, insertedId) } // update connection.Exec( ` update members set valid = @valid where member_id = @member_id` , sql.NamedArg{Name: "valid" , Value: false }, sql.NamedArg{Name: "member_id" , Value: 1 }) // delete connection.Exec( ` delete members where member_id = @member_id` , sql.NamedArg{Name: "member_id" , Value: 1 }) ORMでアクセスする ORMを使う方法もあります。 Goでは、 gorm 、 xorm 、 gorp など幾つか選択肢がありますが、一番メジャー(GitHubスター比較)で、かつ、SQL Serverにも対応しているgormに触れていきます。 https://github.com/jinzhu/gorm gorm パッケージをダウンロードしておきます。 > go get github.com/jinzhu/gorm まずは接続です。 gorm に加えて、 gorm/dialects/mssql をブランクインポートします gorm/dialects/mssql でSQL Server独自の型(bit型など)や処理(SET IDENTITY_INSERTなど)が吸収しています 内部的にはgo-mssqldbをドライバとして使っています 構造体とテーブルレコードがマッピングされます デフォルトではActiveRecordやEntityFrameworkと類似した名前のマッピングが行われます(もちろん変更できます) "構造体名+s"がテーブル名にマッピングされます(Member→members) キャメルケースはスネークケースに変換してマッピングされます(MemberID→member_id) gorm.Open()で接続しますが、第1引数は"mssql"とします ちなみに"sqlserver"は不可です package main import ( "fmt" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mssql" ) // レコードの定義 type Member struct { MemberID int `gorm:"primary_key"` Name string `gorm:"type:nvarchar(256);name:name"` RegisteredAt time.Time `gorm:"type:datetime2;name:registered_at"` Valid bool `gorm:"type:bit;name:valid"` } func main() { connectionString := "sqlserver://ohke:p@ssw0rd@testdb.ikyu.jp?database=TestDB" // 接続 db, err := gorm.Open( "mssql" , connectionString) if err != nil { panic (err.Error()) } // 切断 defer db.Close() // CRUD処理を記述 // ... } CRUDも概観してみましょう。 いずれも上で取得したDBオブジェクトを使います。 1行のselectであればFirst()、複数行のselectではFind()を使います DBオブジェクトを返すので、FindしてDelete、といった処理も書きやすいです // select(1件) var member Member db.First(&member, 1 ) fmt.Println(member.Name, member.RegisteredAt, member.Valid) // select(複数行) members := []Member{} db.Find(&members, "valid=?" , true ) for _, m := range members { fmt.Println(m.Name, m.RegisteredAt, m.Valid) } // insert insertedMember := Member{MemberID: 2 , Name: "akasakas" , RegisteredAt: time.Now(), Valid: true } db.Create(&insertedMember) // update member.Valid = false db.Save(&member) // delete db.Delete(&member) // DBオブジェクトを返すのでメソッドチェーンで繋げることもできます db.Find(&members, "valid=?" , true ).Delete(&members) ここでは紹介しませんでしたが、リレーション定義やマイグレーション等の一般的な機能も提供されています。 おわりに 本投稿では、GoからSQL Serverにアクセスする方法として、生のSQLを実行する方法(go-mssqldb)とORMを使う方法(gorm)を紹介しました。 明日は id:shiba-yan さんによる「一休.com で 1 年半の間に取り組んできた改善内容について」です。
アバター
この記事は 一休.comアドベントカレンダー2017 の6日目です。 一休.comレストラン 検索・集客担当のにがうりです。 一休.com、一休.comレストランともに、検索には主に Solr を利用しています。 (一部、RDBで検索しているところもあります) RDB(SQL)ベースでの検索と比べると色々とメリットがありますが、その中でもファセットナビゲーションに必要な機能が揃っているのは大きな魅力と言えるでしょう。 ファセット例 Solr5.xからは、旧来のファセットとは異なるJSON Facetという機能が新たに提供されており、特に問題(後述の 注意点 を参照)が無いのであれば、こちらのほうが利用しやすいでしょう。 しかし、JSON FacetはSolrのサイト上では言及がなく、 開発者のサイト がドキュメントになっている状況のためか、いまいちマイナーな存在に留まっているように感じます。 このエントリでは JSON Facetについて、旧来のファセットとの比較を混ぜながら、基本的な使い方、応用例、注意点について紹介します。 なお、本エントリで利用しているバージョンはエントリ作成時点の最新版、7.1.0を前提としています。 基本的な使い方 レストランを登録した ikyu-advent-2017-restaurant コアに対し、以下のようなデータが入っているとします レストランID (restaurant_id) レストラン名 (restaurant_name) ジャンル1 (genres) サブジャンル1 (sub_genres) ジャンル2 (genres) サブジャンル2 (sub_genres) 都道府県 (prefecture) 市区町村 (city) 11 AAAA 洋食 洋食-フレンチ 東京都 銀座 12 BBBB 和食 和食-京料理 和食 和食-懐石料理 東京都 赤坂 13 CCCC 洋食 洋食-ステーキ・グリル料理 洋食 洋食-イタリア料理 東京都 銀座 14 DDDD その他 その他-ラウンジ その他 その他-ブッフェ 東京都 品川 15 EEEE 和食 和食-寿司 東京都 銀座 16 FFFF 和食 和食-寿司 和食 和食-天ぷら 東京都 銀座 17 GGGG その他 その他-ラウンジ 和食 和食-寿司 神奈川県 横浜 ※ 以下3点に留意 都道府県 - 市区町村は親子関係であること ジャンル - サブジャンルも親子関係であること さらに、ジャンル-サブジャンルはそれぞれ2つ登録可能であること (MultiValueにしている) 試しに、このデータが入った状態のクエリを実行してみましょう http://localhost:8983/solr/ikyu-advent-2017-restaurant/select?echoParams=none&q=*:*&fl=restaurant_id,restaurant_name,genres,sub_genres,prefecture,city&rows=2 結果 { " responseHeader ": { " status ": 0 , " QTime ": 0 } , " response ": { " numFound ": 7 , " start ": 0 , " docs ": [{ " restaurant_id ": " 11 ", " restaurant_name ": " AAAA ", " genres ": [ " 洋食 " ] , " sub_genres ": [ " 洋食-フレンチ " ] , " prefecture ": " 東京都 ", " city ": " 銀座 " } , { " restaurant_id ": " 12 ", " restaurant_name ": " BBBB ", " genres ": [ " 和食 ", " 和食 " ] , " sub_genres ": [ " 和食-京料理 ", " 和食-懐石料理 " ] , " prefecture ": " 東京都 ", " city ": " 赤坂 " }] } } データが取得できました。ジャンル、サブジャンルは配列で返却されています。 従来のファセットを実行 このデータに対して、従来の方法でファセットを取得してみましょう。 取得対象はジャンル、サブジャンル、都道府県、市区町村の4つです。 (冗長になるためレストラン一覧の取得は抑制) クエリ http://localhost:8983/solr/ikyu-advent-2017-restaurant/select?echoParams=none&q=*:*&rows=0&facet=true&facet.field=prefecture&facet.field=city&facet.field=genres&facet.field=sub_genres 結果 { " responseHeader ": { " status ": 0 , " QTime ": 0 } , " response ": { " numFound ": 7 , " start ": 0 , " docs ": [] } , " facet_counts ": { " facet_queries ": {} , " facet_fields ": { " prefecture ": [ " 東京都 ", 6 , " 神奈川県 ", 1 ] , " city ": [ " 銀座 ", 4 , " 品川 ", 1 , " 横浜 ", 1 , " 赤坂 ", 1 ] , " genres ": [ " 和食 ", 4 , " その他 ", 2 , " 洋食 ", 2 ] , " sub_genres ": [ " 和食-寿司 ", 3 , " その他-ラウンジ ", 2 , " その他-ブッフェ ", 1 , " 和食-京料理 ", 1 , " 和食-天ぷら ", 1 , " 和食-懐石料理 ", 1 , " 洋食-イタリア料理 ", 1 , " 洋食-ステーキ・グリル料理 ", 1 , " 洋食-フレンチ ", 1 ] } , " facet_ranges ": {} , " facet_intervals ": {} , " facet_heatmaps ": {} } } 取得できているのは良いのですが、大きく2つの問題があります。 "prefecture":["東京都", 6,"神奈川県", 1] のように、1つの配列に対して key1, value1, key2, value2 ... という入り方をしているため、処理がしにくい 本来親子関係であるべき、都道府県と市区町村の親子関係が判断できない このうち2についてはジャンル-サブジャンルのように子階層に親階層の情報を付与してあげることで回避可能ですが、1については我慢するしかありません。 しかし、JSON Facetならこの両方が解決できます。 JSON Facetを実行 クエリ http://localhost:8983/solr/ikyu-advent-2017-restaurant/select?echoParams=none&q=*:*&rows=0&json.facet={prefecture:{type:terms,field:prefecture,limit:-1,facet:{city:{type:terms,field:city,limit: -1}}}}&json.facet={genres:{type:terms,field:genres,limit:-1,facet:{sub_genres:{type:terms,field:sub_genres}}}} ※ 都道府県/市区町村のファセット指定を見やすく加工すると以下の通り json . facet = { prefecture : { /* レスポンス時の項目名(任意) */ type : terms , /* ファセットの単位を値に */ field : prefecture , /* ファセットの対象となる項目(都道府県) */ limit : -1 , /* 全件取得 */ facet : { city : { /* ここから子階層 */ type : terms , field : city , /* ファセットの対象となる項目(市区町村) */ limit : -1 } } } } 結果 { " responseHeader ": { " status ": 0 , " QTime ": 9 } , " response ": { " numFound ": 7 ," start ": 0 ," docs ": [] } , " facets ": { " count ": 7 , " prefecture ": { " buckets ": [{ " val ":" 東京都 ", " count ": 6 , " city ": { " buckets ": [ { " val ":" 銀座 ", " count ": 4 } , { " val ":" 品川 ", " count ": 1 } , { " val ":" 赤坂 ", " count ": 1 }]}} , { " val ":" 神奈川県 ", " count ": 1 , " city ": { " buckets ": [{ " val ":" 横浜 ", " count ": 1 }]}}]} , " genres ": { " buckets ": [{ " val ":" 和食 ", " count ": 4 , " sub_genres ": { " buckets ": [ { " val ":" 和食-寿司 ", " count ": 3 } , { " val ":" その他-ラウンジ ", " count ": 1 } , { " val ":" 和食-京料理 ", " count ": 1 } , { " val ":" 和食-天ぷら ", " count ": 1 } , { " val ":" 和食-懐石料理 ", " count ": 1 }]}} /* *** 〜 以下略 〜 *** */ ]}}} ご覧の通り、 {"val":key, "count":value}の組み合わせで表現されているため処理がしやすい 都道府県と市区町村の親子関係が表現できている と、見事に前述の問題が解決できています。 ただし、ジャンル - サブジャンルの親子関係については、「和食」の下に「その他」が混在するという、期待とは裏腹な状態になっています。 残念ながら、こちらは親子関係の親がMultiValueになっている限り回避はできません。 従来のファセット同様個別にファセットの指定を行い、アプリケーション側で親子関係を処理する他無さそうです。 応用例 ところで、一休.comレストランはレストランの「プラン」を予約するサイトです。 つまり、予約検索で表示される一覧は「レストラン」単位ですが、実際に検索しているデータはプラン単位です。 そのため、データもレストランではなくプランが軸になります。 (実際には更に日付、時間、人数、席の有無といった軸も考慮する必要がありますが、複雑になるためここでは割愛します) ikyu-advent-2017-plan コアのデータ id レストランID (restaurant_id) レストラン名 (restaurant_name) ジャンル1 (genre) サブジャンル1 (sub_genre) ジャンル2 (genre) サブジャンル2 (sub_genre) 都道府県 (prefecture) 市区町村 (city) プランID (plan_id) プラン名 (plan_name) 時間帯 (time) 価格 (price) 個室 (private_room) 夜景確定 (nightview) 飲み放題 (free_flow) 11-1101 11 AAAA 洋食 洋食-フレンチ 東京都 銀座 1101 クリスマスディナー ディナー 8000 1 1 0 11-1102 11 AAAA 洋食 洋食-フレンチ 東京都 銀座 1102 クリスマスランチ ランチ 4000 1 0 0 11-1103 11 AAAA 洋食 洋食-フレンチ 東京都 銀座 1103 アフタヌーンティー ランチ 2500 0 0 0 11-1104 11 AAAA 洋食 洋食-フレンチ 東京都 銀座 1104 平日限定スパークリング飲み放題! ディナー 4000 0 0 1 12-1201 12 BBBB 和食 和食-京料理 和食 和食-懐石料理 東京都 赤坂 1201 おばんざいのセット ランチ 3000 0 0 0 12-1202 12 BBBB 和食 和食-京料理 和食 和食-懐石料理 東京都 赤坂 1202 おまかせコース ディナー 7000 1 0 0 12-1203 12 BBBB 和食 和食-京料理 和食 和食-懐石料理 東京都 赤坂 1203 おまかせコース飲み放題付 ディナー 9000 1 0 1 13-1301 13 CCCC 洋食 洋食-ステーキ・グリル料理 洋食 洋食-イタリア料理 東京都 銀座 1301 【ワンドリンク付】プリフィクスランチ ランチ 3000 0 0 0 13-1302 13 CCCC 洋食 洋食-ステーキ・グリル料理 洋食 洋食-イタリア料理 東京都 銀座 1302 極上の短角牛ステーキ300グラム! ランチ 4000 0 0 0 13-1303 13 CCCC 洋食 洋食-ステーキ・グリル料理 洋食 洋食-イタリア料理 東京都 銀座 1303 【飲み放題付き】選べるパスタ・ステーキを含む6種のディナー ディナー 8000 1 0 0 13-1304 13 CCCC 洋食 洋食-ステーキ・グリル料理 洋食 洋食-イタリア料理 東京都 銀座 1304 【Xmas】乾杯シャンパン付!上州牛の極上ステーキとデザートのセット ディナー 3000 0 0 0 14-1401 14 DDDD その他 その他-ラウンジ その他 その他-ブッフェ 東京都 品川 1401 ブッフェランチ ランチ 2000 0 0 0 14-1402 14 DDDD その他 その他-ラウンジ その他 その他-ブッフェ 東京都 品川 1402 【忘年会におすすめ!!】50種類から好きに選べるディナーブッフェ! ディナー 5000 0 1 1 15-1501 15 EEEE 和食 和食-寿司 東京都 銀座 1501 握り10貫 ディナー 7000 1 0 0 15-1502 15 EEEE 和食 和食-寿司 東京都 銀座 1502 握り8貫。お造り、焼き物付き ディナー 8500 1 0 0 16-1601 16 FFFF 和食 和食-寿司 和食 和食-天ぷら 東京都 銀座 1601 握りと天ぷらのコース ディナー 5000 0 0 1 16-1602 16 FFFF 和食 和食-寿司 和食 和食-天ぷら 東京都 銀座 1602 握りのコース ディナー 4500 0 0 1 17-1701 17 GGGG その他 その他-ラウンジ 和食 和食-寿司 神奈川県 横浜 1701 【夜景確定】クリスマスディナー ディナー 9000 0 1 0 17-1702 17 GGGG その他 その他-ラウンジ 和食 和食-寿司 神奈川県 横浜 1702 クリスマスディナー ディナー 7000 1 0 0 17-1703 17 GGGG その他 その他-ラウンジ 和食 和食-寿司 神奈川県 横浜 1703 平日限定ディナー ディナー 5000 0 0 1 このデータに対して、都道府県、市区町村のJSON Facetを実行してみましょう JSON Facetを実行 クエリ http://localhost:8983/solr/ikyu-advent-2017-plan/select?echoParams=none&q=*:*&rows=0&json.facet={prefecture:{type:terms,field:prefecture,limit:-1,facet:{city:{type:terms,field:city}}}} 結果 { " responseHeader ": { " status ": 0 , " QTime ": 0 } , " response ": { " numFound ": 20 ," start ": 0 ," docs ": [] } , " facets ": { " count ": 20 , " prefecture ": { " buckets ": [{ " val ":" 東京都 ", " count ": 17 , " city ": { " buckets ": [{ " val ":" 銀座 ", " count ": 12 } , { " val ":" 赤坂 ", " count ": 3 } , { " val ":" 品川 ", " count ": 2 }]}} , { " val ":" 神奈川県 ", " count ": 3 , " city ": { " buckets ": [{ " val ":" 横浜 ", " count ": 3 }]}}]}}} これはいけません。1行の単位がプランになった関係で、ファセットの数も「プランの数」になってしまいました。 Result Groupingを使いデータをレストラン単位で表現するようにしましょう &group=true&group.field=restaurant_id&group.ngroups=true&group.truncate=true ※ Result Groupingについては本稿の主旨とは異なるため説明は割愛します。 エメラルドアオキロックさんのエントリ がオススメ Result Grouping + JSON Facetを実行 クエリ http://localhost:8983/solr/ikyu-advent-2017-plan/select?echoParams=none&q=*:*&rows=0&json.facet={prefecture:{type:terms,field:prefecture,limit:-1,facet:{city:{type:terms,field:city}}}}&group=true&group.field=restaurant_id&group.truncate=true&group.ngroups=true group.truncate=trueでファセットもグルーピングの単位で返却、group.ngroups=true でグループ単位の検索件数も返却になります。 結果 { " responseHeader ": { " status ": 0 , " QTime ": 1 } , " grouped ": { " restaurant_id ": { " matches ": 20 , " ngroups ": 7 , " groups ": []}} , " facets ": { " count ": 7 , " prefecture ": { " buckets ": [{ " val ":" 東京都 ", " count ": 6 , " city ": { " buckets ": [{ " val ":" 銀座 ", " count ": 4 } , { " val ":" 品川 ", " count ": 1 } , { " val ":" 赤坂 ", " count ": 1 }]}} , { " val ":" 神奈川県 ", " count ": 1 , " city ": { " buckets ": [{ " val ":" 横浜 ", " count ": 1 }]}}]}}} 無事、ファセットの件数がレストラン単位になりました。 プランの情報をJSON Facetで取得 グルーピングはそのままに、プランの情報である夜景確定もファセットで取得してみます クエリ http://localhost:8983/solr/ikyu-advent-2017-plan/select?echoParams=none&q=*:*&rows=0&json.facet={nightview:{type:terms,field:nightview,limit:-1}}}&group=true&group.field=restaurant_id&group.truncate=true&group.ngroups=true 結果 { " responseHeader ": { " status ": 0 , " QTime ": 5 } , " grouped ": { " restaurant_id ": { " matches ": 20 , " ngroups ": 7 , " groups ": []}} , " facets ": { " count ": 7 , " private_room ": { " buckets ": [{ " val ": false , " count ": 5 } , { " val ": true , " count ": 2 }]}}} 夜景確定はプラン毎に異なる情報であるにも関わらず、レストランの数が返ってしまいました。このようなケースでは &group.truncate=true では無理があるようです。 レストラン単位のResult Groupingにプランのファセットも思惑どおり追加する方法 クエリ http://localhost:8983/solr/ikyu-advent-2017-plan/select?echoParams=none&q=*:*&rows=0&group=true&group.field=restaurant_id&json.facet={prefecture:{type:terms,field:prefecture,limit:-1,facet:{restaurant_count:"unique(restaurant_id)"},city:{type:terms,field:city,facet:{restaurant_count:"unique(restaurant_id)"}}}}}&json.facet={nightview:{type:terms,field:nightview,limit:-1,facet:{restaurant_count:"unique(restaurant_id)"}}}}&group.ngroups=true &group.truncate=true を外し、 restaurant_count: "unique(restaurant_id)" を追加しています。restaurant_id でユニークを取った数がrestaurant_countとして返却される、という理屈です。 結果 { " responseHeader ": { " status ": 0 , " QTime ": 3 } , " grouped ": { " restaurant_id ": { " matches ": 20 , " ngroups ": 7 , " groups ": []}} , " facets ": { " count ": 20 , " prefecture ": { " buckets ": [{ " val ":" 東京都 ", " count ": 17 , " restaurant_count ": 6 } , { " val ":" 神奈川県 ", " count ": 3 , " restaurant_count ": 1 }]} , " nightview ": { " buckets ": [{ " val ": false , " count ": 17 , " restaurant_count ": 7 } , { " val ": true , " count ": 3 , " restaurant_count ": 3 }]}}} これで、都道府県 / 市区町村はレストランの数、夜景確定はtrue / falseそれぞれに「該当するプランを持っているレストランの数」が返却されました。 注意点 値の信頼性について 場合によっては大きな問題を招く可能性があります。 SolrのJSON Facetは必ずしも正確なカウント数を返さない ただし、Shardingをしていないかぎりは問題ないはずです。 機能の安定性について 公式に言及が無い機能のため安定性が気になるところでしたが、幸い、導入してから1年ほど安定稼働しています おわりに 以上、いまいちマイナー?なJSON Facetについての紹介でした。 最後に宣伝です。 クリスマスのお店を決め兼ねている方、唐突に忘年会の幹事に指名されてしまった方、是非、一休.comレストランで予約してください。まだ間に合います! restaurant.ikyu.com restaurant.ikyu.com 明日は ohke さんによる GoとSQL Server です。
アバター
この記事は 一休.comアドベントカレンダー2017 の5日目です。 宿泊事業部 Platformチーム *1 の id:minato128 です。今年一休ではクラウド移行に伴い、メール配信の仕組みを大きく変えました。詳しくは 一休✕bitFlyer C#をつかったサービス開発の裏側 でお話したスライドがこちらにありますので、興味のある方はご覧ください。 新メール配信基盤への移行 /ikyu-mail-platform // Speaker Deck さて、宿泊予約やレストラン予約のサービスを提供している一休では、メールをユーザーに届けることはとても大切です。特に予約完了メールが届かなかった場合、(メール以外の確認方法もあるとはいえ)予約が取れたことに気づかず、最悪ユーザーが2重に予約をしてしまう可能性もあります。 *2 そこでメールを届けるために、どのようにメール配信基盤のモニタリングや障害が起きたときのリカバリーを行っているかを紹介したいと思います。 前提として 現在このようにメール配信を行っています。 また、日次のトランザクションメールは 13~15万通ほどです。 各アプリケーションから Cloud Queue (SQS) に Message (JSON) を Enqueue する Worker (Elastic Beanstalk) で Message を処理して SendGrid Web API で送信する 成功したら Message を メールログ(DynamoDB) に保存する n 回エラーになったら Dead Letter Queue (以下 DLQ とします) に入るように設定 SendGrid Event Webhook を AWS Lambda で受けてメールログに送信ステータスを反映する モニタリング このようなアラートを設定しています。大抵の場合、Datadog のアラートで何かが起きていることがわかり、Logentries のエラーログで原因がわかります。 Datadog Worker の状態異常アラート Queue の Message 遅延アラート DLQ の存在アラート Logentries Worker のアプリケーションエラーアラート 障害リカバリー SendGrid だけでなくアプリのバグや AWS など障害によってメールが送れなかったとき、復旧時にメールを再送してあげる必要があるので、このような仕組みを用意しています。 Worker で Message が処理できず、DLQ に入ってしまったとき これはそもそも SendGrid まで届いていないケースで、DLQ から送信用の Queue へ Message を移せるようにしています。Message の移動だけなのでわざわざ用意するほどでもないう気もしますが、障害はいつ起こるかわからないし誰が対応するかもわからないので簡単にできるようにしておいたほうがいいでしょう。 メール DLQ 管理画面 特定条件の Message を再送するとき これは SendGrid まで届いているけれど送れていないかもしれないケース、もしくは単純に再送したいケースで、送信状態や時間帯で DynamoDB からログを抽出して送信用の Queue へ移せるようにしています。 個別再送 送信履歴検索から本文を参照し、再送ボタンを押すとそのメールだけ再送できる 主にCSチームで使用 一括再送 ログを検索して件数とプレビューを表示し、再送ボタンを押すと一括再送できる 大きめの障害のときに使用 その他 送信ステータスが更新されない送信ログの検知 なぜか送信ステータス更新ができていないことが稀にあるため、AWS Lambda で日次実行してログが存在したら通知するようにしています。(SendGrid が不調のときにも発生しますが、正確な原因はまだわかっていません) 施設からユーザー宛のメールがバウンスしたときに、送信失敗お知らせメールを自動送信 ※こちらはモニタリングや障害リカバリーとは関係ありませんが、新しいメール配信基盤の運用開始後にわかったこと *3 として書きます。おまけと思ってください。 前提として、施設さまや店舗さま(ホテルやレストランなど。以下施設とします。)からユーザーへメールを送信する機能があります。 *4 施設管理のメール送信画面 オンプレ時代は Return-Path が施設のアドレスに設定されていたため、施設側がユーザーへのメールが届かなかったときはバウンスメールが返ってきて気づけたのですが、バウンス管理を SendGrid に委譲したためそれができなくなってしまいました。そこで、 SendGrid Event Webhook を AWS Lambda で受けてメールログに送信ステータスを反映する という既存処理のなかに「施設からユーザー宛のメール」かどうかを判定し、送信用の Queue に バウンス通知メッセージを Enqueue するという処理を追加しました。これで送信失敗お知らせメール(バウンスメール相当)を自動送信できるようになりトラブルが減少しました。 明日は @nigauryyy さんの「JSON Facetのススメ」です。 qiita.com *1 : 開発効率を上げるための改善やクラウドインフラ構築、全社共通のAPI開発などを行っています。 *2 : 実際にメールの配信遅延が起こったときにたまに発生します *3 : 考慮漏れとも言えますが *4 : ちなみにユーザーのメールアドレスは施設にはわからないようになっています。(ユーザーがメールを返信しない限りは)
アバター
こんにちは、宿泊事業本部でサービス開発をしている田中( id:kentana20 )です。 この記事は 一休.comアドベントカレンダー2017 の4日目です。 今日は弊社が運営しているサービスの1つである 一休.com のUI改善に関して どのような体制で開発をしているのか ユーザ体験を向上させるために実施していること を紹介したいと思います。 UIチームの体制 12/4(月) 現在、 一休.com では PM兼マーケティング: 1名 デザイナー: 1名 エンジニア: 3名 という体制でUI改善を行っています。 もともとは マーケティング部 デザイン部 システム開発部 と職種ごとに分かれていた組織でしたが、プロダクト開発をより円滑に、スピーディに行っていくために今春から現在の体制に移行しています。 開発組織の変遷 このあたりの体制変更については 12/24(日) に予定している id:sisijumi の「開発組織の目的型組織への移行」にてお話があると思いますので、詳細は今回は割愛します。 どのように取り組むタスクを決めているか 大きめの案件については、CEOや事業責任者と議論しながら決めています。 事業の状況 市場環境 競合の取組み などから、取り組んでいくタスクの大枠が決まります。 一方で、小規模な案件についてはCSやセールスからの改善要望に対して、チームで実施可否と優先順位について意思決定しています。 ユーザ体験向上のために実施していること プロダクトの改善を精度高く行うために、チームで実施していることをいくつか紹介します。 プロトタイピングとデザインレビュー 今日ではプロトタイピングツールも充実し、企画の段階ではコードを書かずにアイデアをメンバー同士で共有する機会が増えてきていますが、一休でもプロトタイピングを実施して 解決したい課題に対して、適切なUIになっているかをレビューする チームメンバーで完成形に対する認識を揃える ということを実施しています。 形式としては対面/オフラインでデザインレビューを行い、指摘事項をGitHub Issuesにまとめて議論しながらフィードバックループを回して目指すUIを決めています。 改善するページ/機能によっては、プロトタイピングツールで表現仕切れないケースもあるので、その場合はHTMLモックを作る場合もあります。 誰かが「こういう形で進めよう」と決めたサイクルではないのですが、いまはこのサイクルで改善が回っていて、徐々に精度が出てきているように思います。 リリース前QA 数日で終わるような改善の場合は実施しないこともありますが、チーム内でのQAをリリース前に実施しています。開発中の作業ブランチを デモ用環境 *1 にデプロイしておいて (初回のみ)その改善で実現したいこと、解決したい課題を改めて共有 各自、思い思いにデモ版を触って使用感を確認 改善したほうが良い内容を発表&議論 やる/やらないを決めて終了 という流れで実施しています。これは弊社のレストランアプリチームで実施している取組みなのですが、「良さそうだ」と思って宿泊UIチームでも導入しました。 QAのメモ 機能の規模や重要度によりますが、このQAを2~3回実施してからリリースする、という流れがスタンダードになりつつあります。 QA#1: 開発が一通り終わったところで実施 プロトタイピング時に決めた仕様が適切か ユーザの課題を解決する改善になっているか 開発した機能にバグ、考慮漏れがないか QA#2: 前回QAでの指摘事項、BugFixを済ませたタイミングで実施 前回QAでの指摘が改善されているか リリースしてよいレベルに仕上がっているか という形で実施しています。β → RC1 → RC2というイメージですね。 QAに関しては 若干コストをかけすぎでは 早く本番にリリースしてユーザの動きを見たほうが良いのでは という意見もあり、今後も最適なやり方を模索していきたいと思っています。 ユーザの行動を体験する 一休ではGoogle AnalyticsとBigQueryを使ってユーザの行動ログを分析しています。このログを元に、課題感のあるページや機能に対して実際のユーザがどのような行動をしているかを、実際に体験するということをしています。 例えば、「主要な導線中のとあるページでの離脱が目立つ」という課題があった場合には 特定ページで離脱しているユーザ1人1人の行動ログを抽出 抽出した行動ログをもとにユーザの行動を1セッションずつトレースする(実際にサイトを動かして体験する) それをひたすら繰り返す ユーザが感じたであろうストレスを考える ストレスの共通点(離脱の理由)を探す という流れで解決するべき課題を洗い出します。課題が発見できたら、解決策として改善のアイデアを議論します。 ユーザの行動ログ 各ユーザのセッションを体験すると 離脱しているユーザの行動を幾つかのグループに分けることで傾向が見えてくる 当初考えていた課題/仮説の裏付けに使える行動を再確認できる などの効果があり、Google Analyticsのサマリや、KPI/ファネルレポートなどを見るのとは違った課題が見えてくることも少なくありません。 すべての改善で実施しているわけではありませんが、「数字的に課題があるのはわかるけど、何を改善していけばよいかが見えてこない」という場合には有効なアプローチだと考えています。 おわりに 今回は一休.comにおけるUI改善の取り組みについてお話しました。いまのチーム体制になってから約8ヶ月くらいですが、徐々にチームとしての形が出来てきていると思う一方で、改善できる部分はまだまだあるので、社内のほかのチームや社外の事例を参考にUI改善の精度を上げる、開発スピードを上げる取り組みを進めていきたいと思っています。 「こんなやり方をしていて、良い具合です」という事例をご存じの方や知見をお持ちの方はぜひご連絡を! プロダクト開発に関して情報共有しましょう。 一休では、ともに 良いサービスをつくっていく仲間(エンジニア/デザイナー/マーケティング)を積極募集中 です。応募前にカジュアルに面談をすることも可能ですので、お気軽にご連絡ください! 明日は @minato128 さんによる「 メール配信のモニタリングと障害リカバリーについて 」です。お楽しみに! *1 : ブランチデプロイ環境で エンジニアが安心できる開発現場へ
アバター
この記事は 一休.comアドベントカレンダー2017 の3日目です。 一休.com の開発基盤をやっています akasakas です。 BeautifulSoup4でスクレイピング スクレイピングでBeautifulSoup4を扱う機会が多いです。 BeautifulSoup4はいろんな便利機能が揃ってますが、自分は全部覚えられないし、使いこなせなません(苦笑) 正直、BeautifulSoup4にあるいくつかのメソッドがそれなりに使えれば、十分スクレイピングできます。 なので、今回はBeautifulSoup4を使い、スクレイピングをして、各種メソッドを紹介します。 スクレイピング対象 一休.com Advent Calendar 2017 - Qiita やってみること カレンダーから日付/担当者/タイトルを取得&出力してみます 結果 day is 1 author is ninjinkun title is 単純なコードでアプリ内のコンバージョン経路を計測する ---- day is 2 author is ryo511 title is 一休.comのJavaScriptユニットテスト環境 ---- ... ---- day is 24 author is zimathon title is 開発組織の目的型組織への移行 ---- day is 25 author is ninjinkun title is 締めます ---- コード from bs4 import BeautifulSoup from urllib.request import urlopen html = urlopen( "https://qiita.com/advent-calendar/2017/ikyu" ) soup = BeautifulSoup(html, "html.parser" ) for advent_calendar_week in soup.tbody: for advent_calendar_day in advent_calendar_week.find_all( "td" , { "class" : "adventCalendarCalendar_day" }): print (f "day is {advent_calendar_day.p.string}" ) print (f "author is {advent_calendar_day.find_all('div')[0].a.get_text().strip()}" ) print (f "title is {advent_calendar_day.find_all('div')[1].string}" ) print ( "----" ) 1つずつブレイクダウン 下準備:htmlパース これだけです from bs4 import BeautifulSoup from urllib.request import urlopen html = urlopen( "https://qiita.com/advent-calendar/2017/ikyu" ) soup = BeautifulSoup(html, "html.parser" ) soupオブジェクトにガツっと結果が入ってます。 このオブジェクトから日付・担当者・タイトルをピックアップします。 カレンダーを取得し、ループで回す for advent_calendar_week in soup.tbody: for advent_calendar_day in advent_calendar_week.find_all( "td" , { "class" : "adventCalendarCalendar_day" }): soup.tbody でtbodyを取得しています。 この中にある <td class="adventCalendarCalendar_day"> が欲しいので、そこから、さらに find_all("td", {"class": "adventCalendarCalendar_day"}) で <td class="adventCalendarCalendar_day">  を全部ピックアップして、ループで回してます。 日付と担当者とタイトル print (f "day is {advent_calendar_day.p.string}" ) print (f "author is {advent_calendar_day.find_all('div')[0].a.get_text().strip()}" ) print (f "title is {advent_calendar_day.find_all('div')[1].string}" ) pタグに日付があるので、 p.string で日付が取得できます divタグの1つ目が担当者、2つ目がタイトルとなります。 個人的に感じたポイント 必要なデータは一括で取得 soup.{tag} で必要なデータは一括で取得できます find_allで必要な情報だけうまく取得する divタグの特定クラスをまとめて取得したい場合にfind_allが便利です。 上の例だと find_all( "td" , { "class" : "adventCalendarCalendar_day" }) で、tdタグの特定クラスだけをまとめて取得しています。 別法 下のリストから日付/担当者/タイトルを取得&出力してみます 結果 day is 12 / 1 author is ninjinkun title is 単純なコードでアプリ内のコンバージョン経路を計測する ---- day is 12 / 2 author is ryo511 title is 一休.comのJavaScriptユニットテスト環境 ---- ... ---- day is 12 / 24 author is zimathon title is 開発組織の目的型組織への移行 ---- day is 12 / 25 author is ninjinkun title is 締めます ---- コード from bs4 import BeautifulSoup from urllib.request import urlopen html = urlopen( "https://qiita.com/advent-calendar/2017/ikyu" ) soup = BeautifulSoup(html, "html.parser" ) for advent_calendar_day in soup.find_all( "div" , { "class" : "container" })[ 4 ]: print (f "day is {advent_calendar_day.find('div', {'class' : 'adventCalendarItem_date'}).string}" ) print (f "author is {advent_calendar_day.a.get_text().strip()}" ) if advent_calendar_day.find( 'div' , { 'class' : 'adventCalendarItem_entry' }) is None : print (f "title is {advent_calendar_day.find('div', {'class' : 'adventCalendarItem_comment'}).string}" ) else : print (f "title is {advent_calendar_day.find('div', {'class' : 'adventCalendarItem_entry'}).get_text()}" ) print ( "----" ) 1つずつブレイクダウン リストを取得し、ループで回す for advent_calendar_day in soup.find_all( "div" , { "class" : "container" })[ 4 ]: divタグの class="container" の中で、下のリストのオブジェクトを取得し、1行ずつ回しています。 投稿済みと未投稿の分類 以下のように、投稿済みと未投稿でタイトルのDOMが少々異なります。 投稿済み < div class = "adventCalendarItem_commentWrapper" > < div class = "adventCalendarItem_entry" > < a data -confirm= "Are you sure to follow a link to this website? http://user-first.ikyu.com/entry/singleton-tracking" href = "http://user-first.ikyu.com/entry/singleton-tracking" target = "_blank" > 単純なコードでアプリ内のコンバージョン経路を計測する < i class = "fa fa-external-link" ></ i > </ a > </ div > </ div > 未投稿 < div class = "adventCalendarItem_commentWrapper" > < div class = "adventCalendarItem_comment" > BeautifulSoup4を実際に使ってみつつ、各メソッドを解説してみる </ div > </ div > <div class="adventCalendarItem_entry"> の有無で判定し、タイトルを取得しています。 if advent_calendar_day.find( 'div' , { 'class' : 'adventCalendarItem_entry' }) is None : print (f "title is {advent_calendar_day.find('div', {'class' : 'adventCalendarItem_comment'}).string}" ) else : print (f "title is {advent_calendar_day.find('div', {'class' : 'adventCalendarItem_entry'}).get_text()}" ) まとめ BeautifulSoup4を使い、スクレイピングをしてみました。 ここでご紹介した機能はごく一部になりますが、それでも使えれば十分スクレイピングができました。 明日は id:kentana20 さんによる「宿泊サービスにおけるUI改善の取り組み」です。 参考 BeautifulSoup4公式ドキュメント
アバター
この記事は 一休.com Advent Calenrad 2017 の2日目です。 宿泊事業本部フロントエンドエンジニアの宇都宮です。 一休.comの宿泊予約サービス(以下、一休)では、以下のようなスタックでWebフロントエンドの開発を行っています。 言語:ES 2017 ライブラリ・ フレームワーク :古いところは jQuery 、新しいところはVue.js ビルドパイプライン:Webpack + Babel 一休では、主要導線のE2Eテストは整備されています *1 。一方、フロントエンド( JavaScript )の ユニットテスト は発展途上といったところです。 本記事では、一休のJS ユニットテスト 環境の変遷と現状について紹介します。 AVA 2017年4月の時点で、一休のJS ユニットテスト 環境は以下のような状況でした。 テスティング フレームワーク :AVA Babelでビルドされているコード:1,000行程度? テストの数:ほとんどない AVA は、Babelによるビルド機能を内蔵したテスティング フレームワーク です。定番 フレームワーク の Mocha に比べて、以下のような特長があります。 暗黙的なグローバルへの依存がない テストが並列に実行される テスト結果の出力がわかりやすい( power-assert ) はじめのうちは、AVAで快適に ユニットテスト を書いていました。しかし、Babelでビルドされるコードが増えるにつれ、 コンパイル 時間が問題になり始めました。 一例として、以下のような簡単な JavaScript コードをテストしてみます。 /** * 金額のフォーマットを行う * ※Number.prototype.toLocaleString() は古いブラウザでは動かないので正規表現を使っている * * Usage: money(10000) => '10,000' * * @param value * @returns {string} */ export default value => String (value).replace( /([0-9])(?=([0-9]{3})+(?![0-9]))/g , '$1,' ); AVAを使うと、テストは以下のように書けます( describe はAVA組み込みの関数ではなく、 ava-spec ライブラリの提供する関数です)。 import { describe } from 'ava-spec' ; import money from 'vue/Filters/money' ; describe( 'Money Filter' , async (it) => { it( '金額をフォーマットして返す' , async (t) => { t.is(money(100), '100' ); t.is(money(1000), '1,000' ); t.is(money(10000), '10,000' ); t.is(money(100000), '100,000' ); t.is(money(1000000), '1,000,000' ); } ); } ); このテストの実行時間を計測すると、以下のような結果になりました。 $ time npm run ava money.test.js √ Money Filter 金額をフォーマットして返す 1 test passed [12:36:03] real 0m35.385s user 0m0.061s sys 0m0.090s 単純なテストなのに 35秒 もかかっています。実行時間のうち、9割以上を占めるのはコードの コンパイル 時間です。AVAは全ての ソースコード をビルドしてからテストを実行開始するため、起動が遅くなっています。 AVAのwatchモードを使えば コンパイル 時間を減らすことはできますが、起動が遅いという根本的な原因は解決されません。 プリコンパイル による高速化も検討しましたが、テストの実行手順が複雑化してしまいます。 そもそも、我々がやりたいのはテストを書くことであって、テストの実行環境を最適化することではありません。ということで、AVAからの移行を検討しました。 移行対象の検討 今後Vue.jsを使ったコードが増えることが予想されるため、移行対象の検討に当たっては、Vue.jsとの相性が良いことを念頭に置きました。 参考(1) vue- cli vue-cli で vue init webpack すると、以下のスタックで ユニットテスト 環境が構築されます。 Mocha アサーション は Chai karma PhantomJS Karmaはブラウザを使ったテストランナーですが、ヘッドレスブラウザのPhantomJSを使うことでCI環境でも実行できるようにしています。 ※以前検討を行った時点では、Karma + Mochaのみでしたが、最新のvue- cli では「Jest」「Karma and Mocha」から選べるようになっています。 参考(2) vue-test-utilsのexample 公式のテストユーティリティ vue-test-utils では、以下のテスティング フレームワーク を使用したexampleを提供しています。 Jest Mocha tape AVA それぞれの実行時間を計測してみました。各exampleで全く同じテストを実行しているわけではないですが、参考にはなると思います。 計測結果は下記で、tapeが最速、AVAは他より倍くらい遅い、ということがわかります。 Runner Time Jest 10.584s Mocha 8.008s tape 6.311s AVA 21.106s tapeが最速で、AVAが群を抜いて遅い、という傾向は、 vue-unit-test-perf-comparison とも一致します。 Runner 10 tests 100 tests 1000 tests 5000 tests tape 2.32s 3.49s 9.28s 38.31s jest 2.44s 4.50s 21.84s 91.91s mocha-webpack 2.32s 3.07s 10.79s 38.97s karma-mocha 7.93s 11.01s 33.30s 119.34s ava 19.05s 73.44s 625.15s 7161.49s 移行対象の決定 移行対象は、下記のスタックにしました。 テストランナー: mocha-webpack webpackでのビルド後にmochaを実行する アサーション :Node.js組み込みの assert を使用 vue-test-utils Mochaにしたのは、vueコミュニティでは最も広く使われている(vue- cli が使っている)ためです。 Karmaは一応導入しましたが、あまり使っていません。 browser-env を使えば、DOMを使っているコードのテストをNode.js上でも実行できるからです。また、Karmaはブラウザの起動コストの分、実行が遅くなるのも懸念点です。 どのくらい改善するか計測する AVAで35秒かかっていたテストが、どれくらいの実行時間になるか計測してみます。 $ time npm run mocha money.spec.js WEBPACK Compiling... WEBPACK Compiled successfully in 963ms MOCHA Testing... Money Filter √ 金額をフォーマットして返す 1 passing (3ms) MOCHA Tests completed successfully real 0m5.217s user 0m0.000s sys 0m0.105s 35秒 => 5秒と、大幅に改善していることがわかります! mocha-webpackを使うことで、テストの実行に必要なファイルだけをビルドしているのが速度の改善に寄与しています。 Mocha移行後のテストコード さきほどの money 関数のテストを、Mocha + assertで書くと以下のようになります。 /* global describe, it */ import assert from 'assert' ; import money from '@js/vue/Filters/money' ; describe( 'Money Filter' , () => { it( '金額をフォーマットして返す' , () => { assert.equal(money(100), '100' ); assert.equal(money(1000), '1,000' ); assert.equal(money(10000), '10,000' ); assert.equal(money(100000), '100,000' ); assert.equal(money(1000000), '1,000,000' ); } ); } ); 見ての通り、describe~itという基本的な構造は変わりません。大きな違いは、 describe と it というグローバル関数が定義されていることが前提になっている点です。この点だけ見るとAVAの方が良いのですが、テストコードの美しさのために開発効率を犠牲にすることはできません…。 今後の展望 ユニットテスト 環境の構築については一段落したので、テストケースの増加や カバレッジ レポートの定点観測といった、テストの網羅性を高める取り組みを進めていきたいと考えています。 明日はakasakasさんによる「BeautifulSoap4を使って スクレイピング しつつ、各メソッドを解説してみる」です。 *1 : 一休.comのE2Eテスト事情 ~ギリギリ話せるところまで話します
アバター