一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

一休.com、Yahoo!トラベルのフロントエンドにカナリアリリースを導入しました

はじめに

宿泊UI開発チームでソフトウェアエンジニアをしている原です。昨年の10月に入社しました。

私の所属する宿泊プロダクト開発部では主に 一休.comYahoo!トラベル を開発しており、今回お話するのは、両サービスのトップページ、施設一覧ページ、施設詳細ページなどの主要な導線のフロントエンドを担う Nuxt.js で作られたアプリケーションのインフラとデプロイについてです。

今回はこのアプリケーションにカナリアリリースの手法を取り入れて、より安全にリリースできるようになった話をします。

カナリアリリースとは

カナリアリリースとは、複数の実行環境を用意しアプリケーションの新旧のバージョンを同時に稼働させ、一部のユーザーに絞って新環境を公開するリリース手法です。 カナリアリリースによって新バージョンに不具合があった場合でもユーザー全体に影響を及ぼすことなく、リスクを低減してリリースすることができます。

導入のきっかけ

一休では昨年から今年にかけて、宿泊プロダクトのNuxt.jsのバージョンを2系から3系にアップグレードしました。 詳しくは 一休.com、Yahoo!トラベルのNuxtをNuxt3にアップグレードしました をご覧ください。

user-first.ikyu.co.jp

上記のブログ記事の「リリース戦略」の項にあるように、メインブランチの内容が反映されているNuxt2バージョンの環境と、検証用ブランチのNuxt3バージョンの環境を立てて、Fastlyでユーザーの振り分けを行っていました。

無事にNuxt3へのバージョンアップが完了し、検証用環境がお役御免になったと思っていたところ、検索フォームの実装をまるごと置き換える大掛かりなリファクタリングが行われました。 同一アプリケーション内で条件分岐によるfeature flagは元来行われていましたが、それを差し込むのも難しいくらい大きな差分が発生するリファクタリングになりました。 そこで影響範囲が大きいリリースになるので失敗のリスクを最小限にしたいと考え、バージョンアップの検証用の環境をカナリア環境と銘打って引き続き使用することにしました。

カナリア環境の実現方法

以下が簡単な構成図です。

カナリア環境を実現するためのシステム構成図
システム構成図

EKS

宿泊プロダクト内のシステムの多くは EKS で稼働していて、この Nuxt.js アプリケーションも EKS で動いています。 クラスタ内に通常バージョンが動作しているものとは別にカナリア環境用のデプロイメントを作成し、そこでカナリアバージョンのアプリケーションを動かします。

Fastly

通常環境とカナリア環境どちらにリクエストを向けるのか振り分けをFastlyで行っています。

以下コード例です(変数やCookieは仮のものです)。

sub vcl_recv {
  // リクエストの振り分け
  if (req.http.Cookie:new-environment-v1) {
      set req.http.new-environment-v1 = req.http.Cookie:new-environment-v1;
  } else {
    set req.http.new-environment-v1 = false;
    // カナリア環境に10%リクエストを振り分ける
    if (randombool(10, 100)) {
      set req.http.new-environment-v1 = true;
    }
    set req.http.new-environment-v1-new-cookie = "new-environment-v1=" req.http.new-environment-v1 "; max-age=31536000; path=/; secure; httponly";
  }
  ...
  // req.backend はfastlyがリクエストを流すオリジンを指定する
  if (req.http.new-environment-v1 == true) {
      set req.backend = new-environment;
    } else {
      set req.backend = normal-environment;
  }
}

sub vcl_deliver {
  // Cookieの付与
  if (req.http.new-environment-v1-new-cookie) {
    add resp.http.Set-Cookie = req.http.new-environment-v1-new-cookie;
    set resp.http.Cache-Control = "no-store";
    unset req.http.new-environment-v1-new-cookie;
  }
  ...
}

大まかな処理の流れを説明すると、

  • リクエストを新旧どちらの環境に向けるのかを識別するCookieの有無を確認
  • Cookieが付与されていなかったら、randombool関数 で一定の割合で新旧どちらに向けるかを決める
  • 新旧どちらかのオリジンにリクエストを流す
  • 新環境へリクエストした場合、次回リクエスト時も新環境へ向けられるようにCookieを付与

という流れになっています。 このアプリケーションは初回リクエスト以降はNuxtサーバーへのリクエストが不要なSPAになっているため、旧環境へ向いていたが動作中に新環境へリクエストしてしまい動作に不具合が生じるといったことも起こりません。 Nuxtのビルド成果物はS3にアップロードしており、アプリケーションが必要とする静的ファイルはカナリアリリースとは関係なく取得することができます。

運用方法

アプリケーションリリース方法

通常バージョンのアプリケーションはreleaseブランチへのマージを契機にCIで自動的にimageをビルド、pushをしてリリースされます。 カナリアバージョンも同様にcanary-releaseブランチへのマージを契機にカナリア環境へリリースされます。

スケールイン、スケールアウト

カナリアリリースを使用していない間はインフラコストの削減のため、podの最小レプリカ数を最小限にしています。 カナリアリリース開始時に流すリクエストの割合に応じてpodの最小レプリカ数を引き上げます。

リクエスト割合の調整

上述のvclのrandomboolの割合を変更します。 この際に割合が正しく反映されるようにnew-environment-v1 といった変数やCookieのsuffixのバージョンもインクリメントさせる必要があります。

以下カナリア環境へのリクエスト割合を10%から0%に変更する際のコード差分の例です。

 sub vcl_recv {
-  if (req.http.Cookie:new-environment-v1) {
-      set req.http.new-environment-v1 = req.http.Cookie:new-environment-v1;
+  if (req.http.Cookie:new-environment-v2) {
+      set req.http.new-environment-v2 = req.http.Cookie:new-environment-v2;
   } else {
     set req.http.new-environment-v1 = false;
-    if (randombool(10, 100)) {
-      set req.http.new-environment-v1 = true;
+    if (randombool(0, 100)) {
+      set req.http.new-environment-v2 = true;
     }
-    set req.http.new-environment-v1-new-cookie = "new-environment-v1=" req.http.new-environment-v1 "; max-age=31536000; path=/; secure; httponly";
+    set req.http.new-environment-v2-new-cookie = "new-environment-v1=" req.http.new-environment-v2 "; max-age=31536000; path=/; secure; httponly";
   }
   ...
-  if (req.http.new-environment-v1 == true) {
+  if (req.http.new-environment-v2 == true) {
       set req.backend = new-environment;
     } else {
       set req.backend = normal-environment;
    }

このように割合変更(カナリア環境へのリクエストを取りやめることも含む)をする度に、コードの修正が必要になります。

カナリアリリース導入の効果

Nuxtをはじめとしたライブラリのバージョンアップや、コード差分が大きく影響範囲の大きなリリースに対するリスクを大幅に軽減できるようになりました。 またこのNuxtアプリケーションは、宿泊事業部内の複数チームが開発しているのですが、カナリアリリースの概要やリリース手順書といったドキュメントの作成をしたり、リリースの都度レクチャーすることで、どのチームもカナリアリリースによって安全にデプロイできるようになりました。

課題

上述の通りカナリア環境へのリクエスト割合を変更するたびにVCLの変数のsuffixを変更する必要があり、そこそこ面倒な作業になっています。 また、カナリアリリース実施中に通常環境のリリースがあった際にその内容をカナリア環境に取り込む必要があります。そのため常に通常バージョンのリリース動向をチェックする必要があり、これもそれなりの負担があります。 これらを自動化などでコストを軽減できないかと模索中です。

最後に

一休ではプロダクトの機能開発をしながらボトムアップで開発基盤の改善もしたい!というエンジニアを大募集中です。 カジュアル面談も実施しているので、お気軽にご応募ください。

www.ikyu.co.jp

hrmos.co

ChatGPTに自社の情報を組み込みたい①

はじめに

こんにちは、一休.comデータサイエンス部の平田です。 みなさんChatGPT活用してますか?

一エンジニアとして便利に使ってはいるものの、自社サービスにどのように組み込もうか模索しているところも多いかもしれません。 一番の利用先として思いつくのが、自社の情報をもとに質問に答えるチャットボットではないでしょうか。

その中では、ハイコンテキストな検索(例えば、「東京から2時間以内で子供も楽しめるアクティビティがあって、景色も良い宿」のような)にも答えられるとボットの価値が増します。 ChatGPTが事前に学習した内容では古く、正確ではないためそういった検索に応えるのはかなり厳しいです。

そのため、こちら側が持っているデータを渡してあげる必要があるのですが、今回はその自社の情報をどう組み込むのか、という部分についてご紹介します。

素のChatGPTでは?

ChatGPTに例えば「熱海で子供も楽しめるアクティビティがあって、景色も良い宿を教えてください」 を聞いてそのまま納得できる内容が返ってくるなら、組み込むだけで済むのでかなり楽です。 まずは試してみます。

熱海は温泉地として有名で、子供から大人まで楽しめる観光地がたくさんあります。以下は、子供も楽しめるアクティビティと景色の良い宿泊施設の一例です。

アクティビティ:
熱海海洋公園: 水族館で、色とりどりの魚やイルカショーなどが楽しめます。
熱海梅園: 季節によっては梅の花が楽しめる庭園で、散策やピクニックが楽しめます。
熱海サンビーチ: 夏場は海水浴が楽しめるビーチで、子供たちに人気です。
MOA美術館: アートに興味を持つ家族には、美術館の散策もおすすめです。

宿泊施設:
アタミセイザンリゾート: 子供向けのプールや遊び場があり、家族連れに人気。部屋からの海の眺めも素晴らしいです。
ホテルニュー熱海: 温泉が楽しめる宿で、子供用のアメニティも充実しています。海を一望できる部屋もあります。
熱海温泉 ホテルマイクローネ: 子供向けのプレイルームやプールがあり、温泉も楽しめます。部屋からの景色も良いです。
これらの施設は、子供向けのアクティビティと素晴らしい景色を楽しめるため、家族旅行にぴったりです。予約時には、子供向けの設備やサービスがあることを確認すると良いでしょう。最新の情報や詳細は、公式ウェブサイトや旅行サイトで確認することをおすすめします。

アクティビティと宿を出してくれるのでぱっと見は良さそうですが、熱海海洋公園はありませんし、宿泊施設に至っては3つともありません。 ChatGPTの学習データは2021年9月までのものなので、かつてはあったのかもしれません。 やはり、最新性のある情報を提供させるために、こちらから与えてあげる必要がありそうです。

情報を組み込む方法

ファインチューニング

質問文と答えの対を大量に用意し、それを事前学習されたモデルに対してさらに学習させることでオリジナルのモデルを作る方法があります。

platform.openai.com

質問文と答えの対を人手で作るのも難しいので、ChatGPTにやらせます。

response = openai.Completion.create(
            engine=COMPLETIONS_MODEL,
            prompt=f"""次の複数の口コミから、50文字以内の日本語の質問文を1つ生成してください。
            \n\nテキスト: {context}\n\n質問文:\n1.""",
            temperature=0.8,
            max_tokens=400,
            top_p=1,
            stop=["\n\n"]
)
prompt=f"次のテキストに基づいて質問に答えてください\n\nテキスト: {row.context}\n\n質問文:\n{row.questions}\n\n答え:\n1.",

ただ、質の高い対を大量に作るのは難しく、学習としてもあまりいい結果になりませんでした。(例はcurieモデル)

上手くいった例
上手く行かなかった例(こちらを向かう海を渡る…?)
8/22にcompletionモデルではなく、chatのモデルである gpt-3.5-turbo-0613 を対象にしたファインチューニングができるようになりました。 chatのモデルで行うともしかしたらいい結果が得られるかもしれません。

(↓見ると難しそうですが…)

ChatGPT の Fine-tuning を試したけど上手くいかなかった話

埋め込みベクトル表現

ファインチューニング以外にも情報を渡す方法としては、プロンプトに必要な情報がまとまった文章を加えておき、 それに基づいて文章を生成してもらうというものがあります。

そのためには、「どの情報を渡すか」という部分をこちらで選択する必要があります。 ChatGPTのtoken数の制限、価格を考慮すると全てを渡すことはできません。 必要最低限の量を渡す方法として、クエリをベクトル化して、あらかじめベクトル化した情報と類似度が高いものだけをプロンプトに加えるようにします。

ファインチューニングと比較したとき、渡す情報をコントロールしやすいメリットがある反面、プロンプトが肥大化しやすいというデメリットがあります。

検証

今回は、口コミの中から必要な情報だけを抽出できるのか?というところをトライしてみます。 ベクトルで類似度をスコアリングしたのち、各項目について言及しているかどうかを正規表現で正誤判定させることにしています。

検証ではOpenAIのembedding APIを使用しています。対象は単語になっていますが、任意の文章をベクトル化することができます。

また、英語だと精度が良いらしいですが、ベクトルマッチングにおいてもそうなのかついでに調べてみます。 ちなみに翻訳にはdeepL APIを使いました。

querys = {'朝食': ['朝食', '朝ごはん', '朝ご飯', '朝御飯', '朝餉', 'ブレックファースト'],
         'ペット': ['ペット', '犬', 'わんこ', 'ドッグ', 'わんちゃん','愛犬','ワンコ','ワンちゃん'], 
         '花火': ['花火'],
         '絶景': ['絶景', '景色がいい', '景色が良い', '景色のいい', '景色の良い'],
         'バリアフリー': ['バリアフリー', '車椅子', '車いす', '足が悪い', '脚が悪い'],
         '有名建築家': ['有名建築家','隈研吾','安藤忠雄','北川原温','坂茂','山口隆','岸本和彦']}

target_query = '有名建築家'
target_vec = get_embedding([target_query])[0]["embedding"]
target_query_eng = translate_text([target_query])[0]
target_eng_vec = get_embedding([target_query_eng])[0]["embedding"]

df['test'] = df.review_text.apply(lambda x: any([q in x for q in querys[target_query]]))    
df['jpn_score'] = df.embedding.apply(lambda x: calc_cossim(target_vec, x))
df['eng_score'] = df.embedding_english.apply(lambda x: calc_cossim(target_eng_vec, x))

ROC曲線は、スコアの閾値を0~1で動かしたときに、横軸に疑陽性の率、縦軸に真陽性の率をプロットしたものです。 下側の面積をAUCと呼び、1なら完全な分類、0.5ならランダムな分類と同程度の精度だと評価されます。

朝食(日本語)
朝食(英語翻訳)

項目 日本語 英語翻訳
朝食 0.83 0.88
ペット 0.92 0.85
花火 0.95 0.97
景色 0.90 0.83
バリアフリー 0.63 0.78
有名建築家 0.95 0.95

単語によって、日本語が良かったり英語が良かったりバラバラですね。 明らかに英語の精度が良くなるかと思ったので意外でした。 有名建築家は正解データ数が少ないので両者高くなっています。

実際の口コミ抽出例

施設、キーワードを入力するとマッチした口コミを返すAPIを作りました。 口コミの一部分を返すことで、プロンプトが長くなるのを防ぐ工夫もしています。

以下はkeyword=温泉をとある施設の口コミを対象に入れたときの一例です。

一つ目は「温泉」がちゃんと入っていますね。二つ目は温泉に近い「温水プール」が入っています。

{
    "hotel_id": "00002627",
    "review_id": "1000022712",
    "review_text": "3連泊させていただきました。主にホテルにこもって過ごすことを前提にお伺いしましたが、客室はとても居心地がよく、朝焼けがとても綺麗に見えました。
また、プールもこじんまりとしていますが十分楽しめました。
特に23時までオープンしていることからナイトプールはとても綺麗なライティングで、夜空も綺麗に眺めることができました。
また、料理のレベルが高く、味も見た目にも楽しめるものばかりでした。
盛り付けはとってもオシャレでした(部屋食もよかった)。
接客は過度なものはなく、他のホテルに比べるとややあっさりした印象ですが、感じの良い方達ばかりでした。
温泉・お風呂は星4つにしたのですが、特段不満に感じることはありませんでした(水圧、綺麗さなどどれも満足)。
館内至るところ、アロマの香りが楽しめたのもよかったです。またお伺いしたいです。ありがとうございました。",
    "score": 0.75787675,
    "matches": [
        {
            "match_text": "温泉・お風呂は星4つにしたのですが、特段不満に感じることはありませんでした(水圧、綺麗さなどどれも満足)。館内至るところ、アロマの香りが楽しめたのもよかったです。",
            "positive_score": 0.89829713,
            "score": 0.75787675
        },
        {
            "match_text": "また、プールもこじんまりとしていますが十分楽しめました。",
            "positive_score": 0.89906454,
            "score": 0.71830595
        }
    ]
},
{
    "hotel_id": "00002627",
    "review_id": "1001226432",
    "review_text": "10月30日宿泊\n客室はゆったりしていて寛げる\n温水プールは温かく景色が良い\n
レストランはおしゃれな空間で\n雰囲気が良い\nスタッフは一生懸命で好感が持てる\n来月またお世話になります",
    "score": 0.7502389,
    "matches": [
        {
            "match_text": "10月30日宿泊\n客室はゆったりしていて寛げる\n温水プールは温かく景色が良い",
            "positive_score": 0.779632,
            "score": 0.7502389
        },
        {
            "match_text": "レストランはおしゃれな空間で\n雰囲気が良い",
            "positive_score": 0.79234755,
            "score": 0.7080128
        }
    ]
}

まとめ

今回は自社の情報をChatGPTに組み込む方法をご紹介しました。

しかし実はまだ、冒頭の「熱海で子供も楽しめるアクティビティがあって、景色も良い宿を教えてください」に応えられるものが出来ていません。 これは、全国の施設情報をベクトルマッチングで一部に絞っていてもなお量が多くてプロンプトに埋め込むことができないからです。

解決方法についてはChatGPTの次の記事でお伝えできればと思います。

また一休では、ともに良いサービスをつくっていく仲間を積極募集中です!応募前にカジュアルに面談をすることも可能ですので、お気軽にご連絡ください!

hrmos.co

一休.com、Yahoo!トラベルのNuxtをNuxt3にアップグレードしました

CTO室プラットフォーム開発チームの山口(@igayamaguchi)です。
プラットフォーム開発チームではさらに内部でプロジェクトチームが分かれており、私はフロントエンド改善チームというチームでリーダーをしています。
フロントエンド改善チームでは主に一休.com、Yahoo!トラベルのフロントエンドの改善を行っております。

今回は一休.com、Yahoo!トラベルで使用しているNuxtのバージョンを2から3にアップグレードしたお話をさせていただきます。

一休.com、Yahoo!トラベルではトップページや検索ページ、ホテル・旅館の詳細ページなど主要なページのフロントエンドはNuxtで開発されています。
NuxtのバックエンドにはGo+gqlgenでGraphQLのサーバーを立てており、NuxtからはApolloを使用してバックエンドと通信を行っています。
このNuxtのバージョンは2となっており、それを今回3にアップグレードしました。

なぜバージョンを上げたのか

話は2022年7月半ばにさかのぼります。
当時は宿泊事業におけるフロントエンド開発の課題について議論をしていました。
一休.com/Yahoo!トラベルではフロントエンドのコードの共通化を行っており、同じリポジトリ内の同じコードで両サイトを実現しています。

user-first.ikyu.co.jp

しかし当初はうまくいっていた一休.com/Yahoo!トラベルのコード共通化、コンポーネント共通化も課題が生まれ、開発速度が大きく落ちてきていました。

上記はフロントエンドの現状のコードの問題をレポーティングしたときの資料から抜粋したものです。
上記画像のようにコードの重複が多く発生していました。(SDとは一休社内におけるスマホを指す用語です)
何か変更をしようとすると一休.com/Yahoo!トラベルとPC/スマホで別々になったコードに変更を加える必要があり変更箇所が4倍になるようになっていました。

この問題に対してデザインシステムを作ることで小さなUIコンポーネントやスタイルの共通化をすることが出来ました。

user-first.ikyu.co.jp

しかしVueの状態やGraphQLに絡んだ処理などロジックに関連する処理の共通化がまだうまくいっていませんでした。
これはうまく抽象化、共通化する手段がNuxt2/Vue2では提供されていないことが問題でした。
Mixinなどを使えば可能ではありますが、Mixinでは見通しが悪くなる可能性が高く暗黙的な抽象化が行われあまりよい形にはなりません。
そこでVue3で登場したComposition APIです。
https://vuejs.org/guide/introduction.html#composition-api
これを使うことで処理の共通化が行いやすくなり、コードの無用な重複も解消することができます。

Nuxt3/Vue3に移行すると他にも利点があり、以下に簡単に記します。

  • 設計を全く違うものにできる
    • Composition APIを使うことができると設計が全く変わる。より良い抽象化、共通化が可能になる
  • 状態管理の選択肢が増える。(useState, Piniaなど)
  • 型が効いて安全に
    • Vue3に上がることでVolar(vue-tsc)と組み合わせて型が効くように
  • 開発環境が爆速に
    • Viteが本当に早い

他にもSuspense、Teleport、ビルドの最適化、などいいことはたくさん。
色々利点がありますがやはり設計が改善できる、ということが大きいです。
Nuxt3の方がより良い方法でコンポーネント、状態管理の設計ができるため移行を決断したのが7月末です。
8月くらいから実際に移行に向けての3人のチームメンバーで作業を進めて2月頭にリリースしました。 (最初期は4人で途中からメンバーの異動があったため基本的に3人)

移行指針

まずは最初に移行の指針を立てました。
それは 最低限のコストで移行する というものです。
Nuxt3にアップグレードする理由はより良い設計にして開発をしやすいようにするためです。
移行しながらきれいな書き方にすることもできますが、その場合移行期間が延びるのと、リリース時にビッグバンリリースとなってしまいます。
移行のコストとリスクを最小限にするためにこの指針を立て、プロジェクト進行時には常にこの指針に従い意思決定を行っていきました。

移行戦略

次に移行戦略を立てました。
Nuxt3/Vue3のドキュメントを読み、Nuxt3/Vue3の破壊的変更を洗い出し、タスクの一覧化、移行戦略を立てました。
移行戦略では以下のことを定めました。

  • Nuxt Bridgeを使うか
  • タスクの切り分け
  • ブランチ運用
  • ミニマムに検証をする
  • リリース戦略

Nuxt Bridgeを使うか

NuxtではNuxt Bridgeというモジュールが提供されています。
https://github.com/nuxt/bridge
Nuxt2に対してこれを導入することで、Nuxt2でありながらNuxt3の機能を利用できる前方互換性のあるレイヤーです。
これを使うことでNuxt2のままNuxt3に対応するコードに徐々に移行するということができます。
しかし私たちはこれを使わない選択をしました。

1つ目の理由はNuxt Bridgeに対応していないモジュールがあることです。
私たちの使用しているNuxt moduleでは、Tailwind CSSなどが例として挙げられます。
そういったモジュールにどういった対応が必要になるのか、そこの対応にどのくらいの時間がかかるか予測できませんでした。
最悪開発が止まりリリースができなくなる可能性がありました。

2つ目の理由は二度テストを行う必要があることです。
上記のモジュール対応もあり、移行時にはサイト全体のテストを行う必要があります。
Nuxt2からNuxt Bridgeに移行するときとNuxt BridgeからNuxt3に移行するときで2度テストを行わなければいけません。

これらの理由によりNuxt Bridgeを使わずNuxt3への移行を行いました。
この決断は後に意思決定もシンプルにすることができ非常によかったです。
あらゆる対応がNuxt Bridge、Nuxt3両方を意識するのではなくシンプルにNuxt3のことだけを考えればよくなりコストが下がりました。

タスクの切り分け

アップグレードをするためにタスクを対応タイミングに応じて切り分けを行いました。
大まかには以下の3つです

  • Nuxt2/Vue2の状態で対応
  • Nuxt3/Vue3の状態で対応
  • リリース前に対応

まず1つ目のNuxt2/Vue2の状態で対応可能なものです。
Nuxt3/Vue3の破壊的変更はNuxt2/Vue2の状態でも対応できるものがあります。
例えばfunctionalコンポーネントがVue3で廃止されたためfunctionalコンポーネントを通常のコンポーネントに変更したり、Vueのfiltersが廃止されたため関数に書き換える、といったものです。

2つ目はNuxt3の状態で対応可能なものです。
1つ目を行ってからNuxt2/Vue2のときに対応できないNuxt3/Vue3の破壊的変更の対応です。
例えばモジュールやプラグインの書き換えです。

3つ目はリリース前に対応するものです。
これはテスト、画面の確認などです。

さらにタスクとは別にこのプロジェクトでやらないことも明記してリスト化するようにしました。
例えばNuxt3/Vue3に移行することでComposition APIが使えるようになるわけですが、「変更を最小限にするために移行時にComposition APIへの書き換えは行わない」、というように理由とともにやらないことを明記してリスト化していました。
実際に作業を進める上でもチームで議論して都度都度これは今やるべきではないとなったものを追加していました。

ブランチ運用

作業を進めるために作業ブランチをどうやって運用するかを決めました。

前述のタスクの切り分けを基に以下のようなブランチ運用を行うようにしました。

  • 破壊的変更に対してNuxt2/Vue2の時点で対応できるものをまず対応してmasterマージ
  • 事前対応できるものが終わり次第Nuxt3移行用のブランチを作成、そのブランチに対して残りの破壊的変更の対応をマージ
  • 各施策のブランチがmasterにマージされた場合、適宜Nuxt3移行用のブランチに取り込み

このようにブランチを運用することでNuxt3移行リリース時にマージするコードの変更は最小限になるようにしました。

ミニマムに検証する

Nuxt3/Vue3を初めて触る際は様々な挙動がわからず悩む時があります。
そういったものを実際のプロダクトで検証しようとすると手間です。
そのため最小限でプロダクションに近しいNuxt3のboilerplateを用意し、そこで様々な実装の検証を行うようにしました。

リリース戦略

本番リリースでは一休.com、Yahoo!トラベルそれぞれを順々に公開するようにしました。

  • 一休.comで10%のユーザーに公開
  • 問題がなければ一休.comで100%のユーザーに公開
  • 問題がなければYahoo!トラベルで10%のユーザーに公開
  • 問題がなければYahoo!トラベルで100%のユーザーに公開

ユーザーの振り分けはFastlyで行い、あるユーザーは常にNuxt2、あるユーザーは常にNuxt3を見るように設定を行いました。
この時、一部のページ、例えばトップページだけNuxt3を公開する、ということはしませんでした。
理由はNuxtがSPA的なJSを使用したページ遷移がありそれを振り分けるのは難しいと判断したためです。

このように移行戦略を立ててNuxt3へのアップグレードを開始しました。

作業を開始する前に

ここまでで戦略を立てて作業を始める準備は整ったのですが、ここでもう一つ作業を開始する前に取り組んだことがあります。
それがNuxt3/Vue3のドキュメントをチームで読み漁ったことです。
プロジェクトを始めた当初はNuxt3/Vue3について解像度が低く当初に洗い出したタスクで過不足がないか、どのくらい時間がかかるのかはかなり不透明だったため、解像度を上げるためにやってみようということで始めました。
毎朝1時間~1時間半ほどチームでZoomで集まりドキュメントを画面共有しながら読みました。
チームメンバーはNuxt2/Vue2の開発経験はあったため、Vueはマイグレーションガイド(https://v3-migration.vuejs.org/ )を、Nuxt3は頭からドキュメント(https://nuxt.com/ )を読んで機能差分について話しながら読み進めました。(当時Nuxt3はマイグレーションガイドが作成途中の項目が薄かったためドキュメントを頭から読んでいます。今なら https://nuxt.com/docs/migration/overview を読めばよいと思います)
結果チームメンバーそれぞれに基礎知識がつきタスクや対応方法の議論が活発になる土壌を作れたためよかった試みでした。

移行作業

ここからは実際に行った移行作業の具体的なお話をします。
とはいえ基本的には前述のNuxt、Vueそれぞれのマイグレーションガイドを読み、破壊的変更となるものを対応しています。
そこで今回、私たちが躓いたものや独自の対応をしているものを紹介させていただきます。

defineComponent/defineNuxtComponent

Nuxt2/Vue2でVueコンポーネントを作成するときに Vue.extend を使っていましたが、Nuxt3では defineNuxtComponent を使う必要があります。
https://nuxt.com/docs/api/utils/define-nuxt-component
今回、破壊的変更は別ブランチを立てて変更をしていくためmasterブランチマージ時に発生する変更は小さくしたいです。
そのためmasterブランチ上でも defineNuxtComponent を使用出来るようにaliasとなる関数を用意しました。

/**
 * defineComponent
 */
import Vue from 'vue'
import { VueConstructor } from 'vue/types'

type VueExtend = VueConstructor['extend']

// @ts-ignore
const defineComponent: VueExtend = (options) => Vue.extend(options)
export { defineComponent }

/**
 * dynamic import
 */
export function defineAsyncComponent(f: Function): Function {
  return f
}

これを用いてmasterブランチに各コンポーネントを Vue.extend から defineComponent に書き換えました。

import { defineComponent } from '~/nuxt3-alias'

export default defineComponent({
  // 中のoption apiはそのまま
})

Nuxt3移行ブランチでは以下のようにaliasを差し替えるだけでよいのでリリース時の変更が最小限になります。

import { defineAsyncComponent as _defineAsyncComponent } from 'vue'

import { defineNuxtComponent } from '#imports'

const defineComponent = defineNuxtComponent
export { defineComponent }

const defineAsyncComponent = _defineAsyncComponent
export { defineAsyncComponent }

fetchフック

Nuxt2ではfetchフックという各コンポーネントで非同期にデータ取得、設定を行うことができる専用のhookが用意されています。 https://nuxtjs.org/docs/components-glossary/fetch/

これがNuxt3では使うことが出来ないようになっていました。 useAsyncData useFetch というものが使えるようになっていてそちらに置き換えが必要です。
https://v3.nuxtjs.org/getting-started/data-fetching
しかし useAsyncData useFetch を使うにはComposition APIへの書き換えが必要です。
これには時間がかかることが予想されたのとmasterブランチとの差分が大きくなってしまうのでfetchフックをそのままにどうにかできないか検討しました。
最終的には以下のようなプラグインを作り、fetchメソッドをそのまま解釈できるようにして対応しました。
難点が1つあって、fetchフックを使用するときはnuxt2FetchKey というキーを定義する必要があるのと、fetchフックを使用するコンポーネントが画面で複数個所で呼ばれないことを前提にしています。

export default defineNuxtPlugin((nuxt) => {
  nuxt.vueApp.mixin({
    async serverPrefetch() {
      if (!hasFetch(this)) {
        return
      }
      if (this.$options.fetchOnServer === false) {
        return
      }
      await this.$options.fetch.call(this)
      if (!nuxt.payload.data) {
        nuxt.payload.data = {}
      }
      nuxt.payload.data[this.$_fetchKey] = this.$data
      this.$_fetchResolve()
    },
    created() {
      if (!hasFetch(this)) {
        return
      }
      this.$_fetchKey = this.$options.nuxt2FetchKey

      this.$_fetchPromise = new Promise((resolve) => {
        this.$_fetchResolve = resolve
      })
    },
    async beforeMount() {
      if (!hasFetch(this)) {
        return
      }

      if (!window.__NUXT__) {
        window.__NUXT__ = { data: {} }
      }

      const serverData = window.__NUXT__.data[this.$_fetchKey]
      if (serverData) {
        Object.assign(this.$data, serverData)
        window.__NUXT__.data[this.$_fetchKey] = undefined
        return
      }

      await this.$options.fetch.call(this)
    },
  })
})

function hasFetch(vm) {
  return (
    vm.$options &&
    typeof vm.$options.fetch === 'function' &&
    !vm.$options.fetch.length
  )
}

使用するコンポーネント側

export default defineNuxtComponent({
  nuxt2FetchKey: "defineNuxtComponentFetchKey",
  async fetch() {
    // 非同期に何かを取得して設定する
  },
});

headメソッド

Nuxt2で使用することができるheadメソッドの内部実装がNuxt3で変わり、computedを解釈することができなくなっていました。
例えば以下のようにheadメソッド内でcomputedを使用しているとundefinedになります。

export default defineComponent({
  head(): MetaInfo {
    return {
      title: this.hoge, // undefinedになる
    }
  },
  comptued: {
    hoge() {
      return 'hoge'
    }
  },
})

これに対応するために独自にプラグインを作り、Nuxt2と同じような動きをできるように、 xxx.call(this) によるVueコンテキストを注入しての実行、実行後の値をwatchにより監視し、変更後に useHead を使用してmeta情報への適用をするようにしました。

export default defineNuxtPlugin((nuxt) => {
  nuxt.vueApp.mixin({
    data() {
      return {
        head: undefined,
      }
    },
    created() {
      if (!this.$options.oldHead || this.$options.head) return
      this.$watch(
        () => {
          return this.$options.oldHead.call(this)
        },
        // @ts-expect-error
        (newValue) => {
          this.head = newValue
        },
        { immediate: true, deep: true },
      )
      this.$_fetchPromise?.then(() => {
        this.head = this.$options.oldHead.call(this)
      })
      useHead(() => this.head)
    },
  })
})

コンポーネントを使用する側は head というメソッドを oldHead というメソッドに書き換えるだけで正しくcomputedが解釈できるようにしました。

export default defineNuxtComponent({
  oldHead() {
    return {
      title: this.hoge,
    }
  },
  comptued: {
    hoge() {
      return 'hoge'
    }
  },
})

他にもいくつか問題があり対応をしています。

  • Apollo
    • NuxtのApolloモジュールは現在Nuxt3対応がされていますが、つい最近まではNuxt3に対応していませんでした
    • vue-apolloを直接使用しNuxtのプラグインとして独自実装
  • Storybook
    • NuxtのStorybookモジュールは現在もNuxt3は未対応のまま
    • @storybook/vue3を使い独自に実装
  • axiosの脱却
    • ApolloのRestLinkへ移行

他にもここには書ききれないほどに様々な対応をしていますが、この記事では省略させていただきます。
私たちはかなり早い段階でNuxt3対応を始めたことでNuxt3のバグを踏むということも多々ありました。
今であればバグも減り、ドキュメントも整ってきているのでもう少し早く移行を進められるかもしれません。
しかし早い段階で対応できたことで本来実現したかった設計の改善に着手できており、この面ではよかったと思っています。

終わり

現在はNuxt3を使って更なる改善に挑戦中です。 この記事がこれからNuxt3へのアップグレードを行う方々の力になれば幸いです。

一休では一緒に働く仲間を募集しています。まずはカジュアル面談からお気軽にご応募ください!

hrmos.co

宿特化の写真投稿SNS「YADOLINK」のUIUX設計 について

YADOLINK事業部デザイナーの李と申します。

YADOLINKは、一休が運営する「ホテルや旅館など”宿”が大好きな人たちが集まるSNS」です。宿に特化したサービスだからこそ宿泊体験を気兼ねなく投稿でき、深い共感を得られます。 web版を2022年4月19日に公開し、iOSアプリを2023年1月24日(火)にローンチしました。

YADOLINK by 一休.com

YADOLINK by 一休.com

  • IKYU Corporation
  • 旅行
  • 無料
apps.apple.com

1. 写真投稿SNSの “ベーシック” を考える

どこまでInstagramに寄せるべきか?

YADOLINKは写真投稿が主要な手段のSNSです。広く知られている写真投稿SNSにInstagramがありますが、「インスタっぽいものだよ!」と簡単に伝えることで、最初の利用するハードルが下がると考えました。どのようにInstagramに寄せて、どのように差別するかを最初に検討しました。

フィードUI

Instagramの最も基本的なUIはホーム画面に表示される通常の投稿フィードです。

多くのユーザが使い慣れているUIであるため、YADOLINKでもユーザーの投稿をフィード形式にすることを決定しました。しかし、Instagramのフィードをそのままにするのではなく、YADOLINK独自のユーザーニーズに合わせて、情報整理を行い、以下の部分を差別化することにしました。

1. 宿に特化したサービスのため、宿泊施設が一番重要な要素となり、フィードに宿泊施設名を表示するように追加した。

2. 投稿全体に対しての説明だけでなく、写真ごとに説明&思いを記述できるようにキャプションを追加した。

3. 「いいね」をするハードルを下げるため「いいね数」だけを表示して、「いいね」をした人のリストは非表示した。

4. ユーザーの体験をよりスムーズにするため、ページ遷移せずにコメントできるようにコメントのUIをモーダルにした。(アプリのみ)

5. 初期リリース時には、お互い知らない人同士の投稿にいいねやコメントすることのハードルが高いと思われるため、いいね数やコメント数が少なくても投稿するモチベーションを高めるため、「閲覧数」という指標を表示することを決定した。

2列フィードUI
中華圏を中心とした新興SNSを参考に

初期リリース時にはフィードUIのみが提供されましたが、一覧性が低く1スクロールで1~1.5の投稿しか見られない、タイムラインに流れている投稿が自分がフォローしている人の投稿に限らず全ての投稿が表示されるなどの課題がありました。

この課題を解決するために、下記の2つの改善を行いました。

1. YADOLINKでは中華圏を中心とした新興SNSでよく見られる「2列フィード」のUIを導入しました。これは、生活必需品とは異なり、宿みたいな嗜好品を「気になるからとりあえず見てみたい(予約意欲までいかない場合も含む)」という場合、ユーザーのニーズが漠然としていることが多いです。「行きたい宿を探す」というより「漫然とユーザーの投稿を眺めている内に行きたい宿を見つける」という仕様がYADOLINKユーザーに最もマッチすると考えました。「行きたい宿が決まっている」場合は、YADOLINKよりも一休.comの方を利用するユーザーが多いかもしれません。「小紅書」という中国のSNSの2列フィードUIは、このようなユーザーの気持ちにとことん寄り添い、参考になりました!

※「小紅書」(RED) は、2022年2月時点、登録ユーザー数3億人の動画や写真などを共有できる「インスタグラムとアマゾンが合体したようなアプリ」です。

2. 2列フィードUIの導入に伴い、トップページは「おすすめ」「フォロー」「新着投稿」の三つのタブに分けられ、それぞれがタイムラインとして閲覧できるようになりました。

宿の写真が引き立っていることを一番大事に

一休.comというサービス従来の上質感を残しつつ、新しいサービスとしての活発・躍動感も表現できるようにデザインしています。

主役である宿の写真が引き立っていることを一番大事にしています。InstagramなどのSNSで流行っている写真の上に文字入れインパクト系の画像は上位表示させないなど、一休.comの上質な世界観を取り入れています。

2. SNSは “アプリが命”

YADOLINKはWebからスタートしましたが、最初からアプリを意識していました。なぜならば、SNSは「アプリが命」だと考えられているからです。頻繁に開かれるものであり、毎回ブラウザを開くことは大変面倒です。「いいね」「コメント」「フォロー」などの機能にもリアルタイムのプッシュ通知が不可欠です。アプリではウェブサイトで実装が難しいインタラクションも実現できるため、ユーザーの没頭を促し、ストレスなく操作することが期待できます。

下記の図のようにアプリでは
・スワイプでタブ移動、ページ遷移できるようになった
・インタラクションでおすすめ投稿一覧と投稿詳細間がスムーズな往来になった
WEB
 
APP
・ログインフローのUIUX改善
WEB
 
APP
アプリ開発に伴い、その他の改善点も整理してみました。興味がある方ぜひ触ってみてください〜
・新しく実装された関連投稿で次々に行きたい宿が見つかる
・いいねの傾向に基づいたおすすめ投稿の精度向上
・写真選択段階に並び順を変えられるように
・投稿画像の縦横方向を指定できるように
・トリミング時の画像拡大・縮小
・入力画面にてスクロースせずに全ての項目を入力できるように
・キャプションの追加などはページ遷移→モーダルに変更
・自分の投稿へのコメント通知により素早く返事ができるように
・他にも自分への投稿へのいいねやフォロー通知が受け取れる
・受け取りたい通知はカテゴリごとに制御可能

3.まとめ

「YADOLINK」は、お宿を訪れた方が自分で撮影した写真を投稿できる「宿特化型SNS」です。写真を中心に、ユーザーが直感的に好みの宿を探しやすいインターフェースを提供しています。自分自身では言語化できないニーズを発見したり、まだ知らなかった「自分好みのお宿」との偶然の出会いを演出します。

これまで一休にはSNSサービスがありませんでしたが、YADOLINKをきっかけに新しいUIUXに取り組み、試行錯誤しながら多くのことを学びました。

特定のジャンルを絞らないSNSとは異なり、宿泊施設に特化したSNSであるため、ユーザー層や利用頻度などが比較的限定的ですが、UIUXの改善により、より多くのユーザーに愛用していただけるよう努めています。

一休では、ともに良いサービスをつくっていく仲間を積極募集中です。応募前にカジュアルに面談をすることも可能ですので、お気軽にご連絡ください。

hrmos.co

本社を東京ガーデンテラス紀尾井町へ移転し、オフィスファシリティ・コーポレートIT を刷新した話

はじめに

社内情報システム部 / CISO室 所属 コーポレートエンジニアの 大多和(id:rotom)です。

2022年12月5日、一休は本社オフィスを港区赤坂から千代田区紀尾井町の東京ガーデンテラス紀尾井町 紀尾井町タワーへ移転しました。 ヤフーや PayPay、ZOZO をはじめ、Zホールディングス各社やデジタル庁も入居するビルです。

新オフィスのコンセプト、概要についてはプレスリリースをご覧ください。

prtimes.jp

当社は2022年4月に働き方を刷新し、オフィスワークとリモートワークのハイブリッド制を導入しました。従業員がより高いパフォーマンスを発揮できるよう、オフィスワークの日を職種ごとに週1・3・5回に分けています。移転後の座席は、出社回数に応じて、フリーアドレス・グループアドレス・固定席の「ハイブリッド」とし、オフィス勤務時には従業員同士が円滑にコミュニケーションを取れるよう設計しました。

一休は2022年4月よりリモートワークとのハイブリッド勤務となるように働き方が大きく刷新されました。

本社移転に伴い、ただ現状のインフラのまま引っ越すだけではなく、上記のような時勢に合わせた新しいオフィスファシリティの構築、PBX や FAX のほかオンプレミスサーバーを撤廃した、クラウドネイティブでモダンなコーポレートIT への刷新も行いました。 今回はその取り組みの一部をご紹介します。

この記事は corp-engr 情シス Slack(コーポレートエンジニア x 情シス) #1 Advent Calendar 2022 15日目の記事でもあります。他の素晴らしい記事は以下のリンクからご覧ください。

adventar.org

オフィスファシリティ

まずはオフィス環境についてご紹介します。一休の前オフィスは5階・6階・7階の3フロアに分かれていましたが、今回の移転で10階のワンフロアに統合されました。 これまでの3フロアの床面積を合計しても、なお新オフィスのワンフロアのほうが広いという一休史上最大規模のオフィスです。

執務室

執務室はエンジニアに限らず、営業、バックオフィスなどを含む全ての座席にエルゴヒューマンのチェアが設置されています。 後述しますが、今回の移転のタイミングで各席に置かれていた固定電話を廃止し、机上への配線は OA タップのみという非常にすっきりしたデスクになりました。

エンジニアエリア

前述の通り、職種に応じてオフィスへの出社日が決まっており、エンジニア職は週1 オフィスワークの職種です。 そのため、各事業部のソフトウェアエンジニアやデータサイエンティストなどの席は、従来の固定席を廃止しフリーアドレス化しました。

エンジニア席には LG 製の 4K ディスプレイが標準で配備されています。USB PD(Power Delivery)給電に対応しているため、出社時も充電アダプタを持ち歩く必要がなく、USB-C ケーブルを接続すればモニタへの出力と充電を同時に行うことができます。

会議室 / 個室ブース / 集中スペース

コロナ前は会議室で対面で行っていたミーティングは、コロナ禍に Zoom や Google Meet を利用したオンラインミーティングへと移行しました。 そのため、新オフィスでは大人数向けの会議室より、1on1 などで利用できる小規模の会議室や、遮音性の高いオンラインミーティング用の個室ブースや、囲いで覆われた集中ブースで多く設置されています。

会議室の壁はホワイトボードになっているため、ブレインストーミングなどに最適です。

オフィスは非常に広く、座席も大量にあるのですが、エンジニアにはここが一番人気のようです。

また、窓際には予約不要で利用できるフリースペースは多く設置されており、ちょっとした打ち合わせにサクッと利用できます。

晴れた日のカウンター席は景観がよく、素晴らしい夕日が差し込むおすすめスポットです。

ラウンジ

社内で「ラウンジ」と呼ばれているのが休憩や食事、社内イベントなどに利用されるリフレッシュスペースです。写真の「イソテーブル」と呼ばれる什器はヤフーで使われていたものを譲り受けました。新オフィスのコンセプト「サステナビリティ」を体現しており、早速ティーブレイクやちょっとした打合せで活用されています。

前オフィスでは、コロナ前はラウンジのあったフロアに全就業員が集まり、経営陣からの実績報告やケータリングの料理やお酒を楽しむパーティーが開催されていました。 また、技術コミュニティのIT勉強会や、外部の経営者をお招きした MANABIBA と呼ばれる経営対談も活発に行われていました。

これらのイベントはコロナ禍では YouTube Live や Zoom ウェビナーを活用した配信方式への切り替わっていました。 オフィスワークとリモートワークのハイブリッド制となった今、このラウンジも会場・配信の双方に最適化されたAV システムに刷新しました。

社員食堂 / カフェテリア / コンビニ / コワーキングスペース

東京ガーデンテラス紀尾井町 紀尾井タワーはヤフーをはじめ、Zホールディングス各社が多く入居するオフィスビルです。 一休も同じビルへ同居することでヤフーのオフィス設備の一部を利用させていただいています。

1つ上の11階にはBASE11と呼ばれる社員食堂・カフェテリアがあります。紀尾井町は前オフィスの赤坂と比べ飲食店街からは少し離れた場所に位置するため、外食をするには移動しなければなりません。オフィス内に社員食堂があることで、安くて美味しいバランスの採れた食事を日替わりで食べることができ大変便利です。

about.yahoo.co.jp

同フロアにはZホールディングスのアスクル・出前館が運営するコンビニ Yahoo!マート の店舗もあり、オフィスから出なくても食事や日用品を購入することもできます。

about.yahoo.co.jp

また、コロナ前は一般の方へも公開していたコワーキングスペースの LODGE は現在はZホールディングス従業員向けに公開されています。

lodge.yahoo.co.jp

一休のオフィスはワンフロアですが、このようにヤフーのオフィス設備も利用させていただくことで快適に業務を行える環境が整っています。

コーポレートIT

ここからはコーポレートITの話をします。移転前のオフィスにはサーバールームがあり、オンプレミスで稼働しているサーバーが複数ありました。 これらのオンプレミスサーバーは以前より AWS への移行や SaaS へのリプレイスなどクラウド移行を継続して行っており、移転前の時点ではプロキシサーバーと PBX を残すのみとなっていました。

今回の移転のタイミングでプロキシサーバーをクラウド移行を完了し、固定電話も廃止することで PBX を撤廃し、新オフィスからはサーバールームが無くなりました。

電話 / Dialpad Enterprise

前オフィスではサーバールームに PBX が設置されており、事業部や本部ごとに電話番号を持ち、各席には固定電話機が設置されていました。 コロナ感染拡大に伴い、全社的に在宅勤務の体制が取られた際に、コールセンターや一部部署で先行して利用していたピュアクラウド型のビジネスフォンシステム Dialpad を全社導入しました。

www.dialpad.co.jp

固定電話の番号を Dialpad で発番した 050 の番号に即時転送をかけることで、オフィスに出社することなく、在宅でも受電対応が行える環境を構築しました。 このときの取り組みは大規模な導入になったため、Dialpad Japan の導入事例としてもご紹介いただいております。

japan.blog.dialpad.com

Dialpad はフルクラウドであることから非常に管理がしやすい一方で、IP 電話であるため電話番号が 050 番号でした。 当時は 050 番号から 0120 を始めとするフリーダイヤルへ発信できないことや、03 番号への着信の転送への折返しの際も先方には 050 番号が表示されてしまうため出ていただけないことがある、といった課題感もありました。 この課題を解決するためには自社でゲートウェイを設置する必要があり、フルクラウドである恩恵を得られにくいものでした。

www.softbank.jp

2021年9月に Dialpad Air 0AB-J という新オプションが登場し、ゲートウェイなどの機器を設置不要で 03 番号(0-ABJ)が利用可能になったことから、今回の移転のタイミングでオンプレ PBX を脱却し、Dialpad へ一本化することを決めました。 この際、利用部門が大きく拡大することからグループ数が無制限となる最上位プランであり、Azure AD による SAML/SSO や SCIM にも対応した Enterprise にアップグレードを行いました。

現在は元から固定電話のないエンジニアをのぞく、全ての部門で Dialpad を利用しています。

FAX / FAX PLUS Enterprise

PBX を同じく、オフィスに設置されているオンプレミス機器として FAX の存在がありました。 事前のヒアリングから事業部、管理本部ともに FAX はすでに業務でほぼ利用されていないことは分かっていましたが、官公庁やビルの防災センター、クレジットカード会社など、一部 FAX でしか受付を行えない組織との取引手段として、FAX は今後も残す必要がありました。

従来の FAX はアナログ回線を引き込み複合機から送信するものでしたが、これもオフィスからではないと送受信が行えないため現在の働き方には合っておらず、フルクラウド型へ移行しました。

選定したのは FAX PLUS というスイスのオンライン FAX サービスで、マルチプラットフォームかつ Web アプリから利用可能なものです。

www.fax.plus

海外 SaaS ではあるのですが、日本の FAX 番号を取得することが可能です。2022年12月時点では 050 番号のみ利用可能で、03 番号などの 0-ABJ は利用できません。 050 番号の取得には日本の法律により審査が必要で、登記簿謄本や代表者(または委任を受けた担当者)の本人確認書類などの提出が求められます。サポートとは英語でのやりとりなので難易度は少し高めでした。

FAX PLUS も最上位である Enterprise プランを契約し、Azure AD による SAML/SSO や SCIM を構築しています。

また、Slack とのインテグレーションが可能であり、FAX を受信した際は Web アプリを開くことなく、Slack のチャンネルから直接 PDF で閲覧・ダウロードが可能です。

入退室管理 / Akerun コントローラー

オフィスの入退室を管理するセキュリティ製品も、移転前にオンプレミスサーバー型のものからクラウド型のものへリプレイスを行っていました。Akerun コントローラーという製品です。

akerun.com

Akerun というと、WeWork などのレンタルオフィスに工事不要で設置ができる Akerun Pro のイメージが強いかもしれません。 今回選定している Akerun コントローラーは、専門業者の工事により、既設のオフィスビルの電子錠や自動ドアにも対応したものです。

corp.teamspirit.com

また、Akerun は勤怠管理システムである TeamSpirit と API 連携を行うことが可能であるため、オフィスに出社した際には出勤・退勤が自動打刻されるように設定を行いました。 一休は北海道から沖縄まで多くの拠点を持ちますが、全ての拠点で Akerun へのリプレイスが完了しており、勤怠打刻の自動化は多くの従業員に喜んでいただけています。

終わりに

今回の移転ではモダンなオフィス・コーポレートIT環境へ一気に推進することができており、キラキラなオフィス環境にも見えるかもしれません。 一方で、タイトなスケジュールの中でのオフィス移転は、想定外のトラブルも含め多くの課題もあり、まだまだ改善・進化の余地が残されています

情シス / コーポレートエンジニアとして、エンジニアを含む従業員体験を向上を目指し、皆さんがより快適に業務を行うことができるオフィス・コーポレートIT 環境を目指して、引き続き全力でやっていきます 💪

突然ですが、ここで CM のお時間です

一休ではソフトウェアエンジニアをはじめ、多くの職種で積極的に採用を行っています。 選考をともなわないカジュアル面談からも受け付けておりますので、お気軽にご応募ください 👋

www.ikyu.co.jp

エンジニア採用の Twitter アカウントも開設し、イベント告知などを発信しています。こちらもフォローお待ちしております。

twitter.com

また、今回ご紹介した話はコーポレートITの取り組みの一部です。より深堀りした内容を Business Technology Conference Japan(BTCONJP) というITのカンファレンスでもお話させていただく予定です。 ご都合の合う方はこちらも是非オンラインにてご参加ください 🙏

btcon.jp

新サービス「一休.comふるさと納税」でデザインシステムの活用とFigmaを使いました

プロダクト開発部デザイナーの安松と申します。

10/3、新サービスの「一休.comふるさと納税」がローンチしました。
選んだ宿がある自治体に寄附をすると、一休.comで使える割引クーポンを返礼品として、web上で受け取れるというサービスです。

一休.comの宿泊予約とは違ったサービスですが、予約へとつながるサービスをどのようにデザインに反映させたか、また一休.comの宿泊デザインシステムの活用やFigmaを使ったことを振り返ります。

目次
1) ふるさと納税サイトで意識したこと
2) 宿泊のデザインシステムを活かせるか
3) Figma導入後、初のゼロからデザイン
4) まとめ

1. ふるさと納税サイトで意識したこと

サービスコンセプトのすり合わせ

モックを作成するにあたりビジネスサイドから、サービスの概要やターゲット層に加え、一休.com(宿泊)のUIを活かしたい、とにかくシンプルに…などの要望をもらいました。また、今回のプロジェクトは、短期期間での開発ということもあり、主要な導線の要素が大筋決まっていたので、画面イメージを早く作り詳細をすり合わせながら「どんなユーザー体験にしたいか」のヒアリングや掘り下げ、他のデザイナーからのフィードバックを受けながらの作成です。

デザインにどう反映したか

まずは既に一休.comを使っていただいているユーザーがターゲットです。どのようにデザインに反映したかまとめます。

ユーザーが「寄附」と「予約」を混乱しないように
宿泊と同様に宿から選ぶのですがあくまで「寄附」をするサイト。宿の情報などは最低限にして「予約」に関する宿の情報やプランは、宿泊サイトで確認していただくような作りです。行き来することも考え、メインとなる画像の見せ方も差別化しました。

宿のカードも差別化のため、ベースは活かしつつ意図的にデザインを変更しています。

シンプルかつスムーズに
情報もシンプルにしたので、使用する色も少なくし、目立つアクセントカラーをCTAボタンとして寄附完了までの道しるべを明確にすることで、迷わずスムーズに手続きいただけるよう意識したポイントです。

安心して寄附していただけるように
高額の寄附かつ変更やキャンセルができないため、特に寄附やクーポンに関する注意事項の配置や寄附金額・返礼品としてもらえる割引クーポン額の表示には気を付けました。

2. 宿泊のデザインシステムを活かせるか

ふるさと納税サイトを着手するの少し前から、宿泊ではデザインシステムの導入を始めており、デザインツールをXDからFigmaに移行し、デザインコンポーネントを大小さまざまな粒度で作成しています。こちらを活用することで宿泊のUIを活かしつつ、ふるさと納税サイトを組み立てようと考えました。実装上はコンポーネント化されておらず、宿泊のように、デザインとコードの連動はしていません。

実際にどの部分を採用したか一部をご紹介します。

タイポグラフィー
一休らしさを担うタイポブラフィー全般(書体・サイズ・太さ)はそのまま使用しています。

フォームのUI・パーツ
一休.comのユーザーが使い慣れているUIを活かすため、入力画面や決済画面はページ全体、ラジオボタンや入力フォームなどのパーツもほぼそのまま取り入れています。

タブデザイン

実装面でも宿泊と同様のTailwind CSSを採用していたこともあり、角丸やシャドウ、padding/marginなどの細かい部分も同じように使えたのもよかったことです。

このように別サービスでも、一休.comの宿泊デザインシステムで定義したデザインの基本要素や粒度の細かいコンポーネントが活用でき、「一休.comのUIを活かしたい」にも繋がったのです。また、活用したことで統一や上記で挙げた部分を考える時間は、他の作業に当てることができました。

user-first.ikyu.co.jp

3. Figma導入後、初のゼロからデザイン

宿泊でFigmaを使い始めた際、ツールの使い方に慣れず苦悩していました。さらに、既存サービスである宿泊デザインシステムではどのコンポーネントにするかを現状のデザインを見ながら考えていましたが、今回は新規サービスであったため、現状のデザインはない状態で新しいデザインの検討やパターン出しをすることに苦労しました。慣れないツールと新規サービスという2つの難しさが重なったことが辛かったです。

テキストスタイル・色の定義で大失敗、慣れるまで時間がかかった
デザインする中で、宿泊のコンポーネントを変更して使いました。それは問題のないことですが、ふるさと納税側で新しく定義する色を試行錯誤している段階では、Color Stylesで定義せずに進めていました。

結果様々な画面でパターンを出した後に定義を追加したため、作成済みのモックで色やテキストの置き換え作業が発生してしまいました。

仮の状態で後から微調整をするからこそ、定義をしておくべきでした。また、色の選考の際は定義したカラーリングごとにフォルダを作るなどをして、Figmaの便利な機能を活用できそうです。

コンポーネントも散らかり気味で、同じようなものができたり、手動で置き換え作業も何度もしましたが、最終的にページを横断して使うコンポーネントはまとめて管理し、ライブラリとして公開した後は、変更を加えると反映される状態になっていきました。ある程度デザインが固まったときにコンポーネントの見直し・整理をするというのも大事かもしれません。

4. まとめ

デザインに入る前に「どんなユーザー体験にしたいか」を掘り下げ事前に話し合ったことで、新しい機能や改善の際も、検討中の時にそれがぶれていないか?と一つの指標になり、チームが同じ方向を向けたと思います。

また、ふるさと納税では、宿泊のデザインシステムがあって本当によかったと感じています。統一感やスピードの面で活かすことができたためです。ただ、別サービスでも使用するという目的で作られていないため、今後の扱いは改めて検討する必要はありそうです。

そんな中、一休のサービスを横断したガイドラインを作成するプロジェクトが、デザイナー主体で始動しました。どのサービスを使ってもユーザーにとって素晴らしい体験ができるよう、さらに進化をしている最中ですので、今回の経験が少しでも役立てばよいなと思います。

一休では、ともに良いサービスをつくっていく仲間を積極募集中です。応募前にカジュアルに面談をすることも可能ですので、お気軽にご連絡ください。

hrmos.co

閲覧メインのページを検索メインのページに統合しました

こんにちは、プロダクト開発部の野口です。

一休にはたくさんの施設紹介ページがあるのですが、その中でもキュレーションページという流入数が高いページがありました。それをメイン動線であるリストページに統合したので、その経緯や裏側をご紹介します。

一休の施設掲載ページはたくさんある

一休には施設をまとめて掲載するページがたくさん存在します。

リストページ(メイン動線)

https://www.ikyu.com/area/ma000000/t105/si1/?adc=1&asc=01&cid=20221008&cod=20221010&hoi=1,2&lc=2&mtc=003&per_page=20&pn=1&ppc=2&rc=1

キュレーションページ

https://www.ikyu.com/theme/t105/

観光ページ

特集ページ

などなど。。。

その中でも今回はキュレーションページをリストページに統合したというお話です。

どうして統合したの?

キュレーションページは検索するためのページというよりも、検索エンジンからの流入口として用意されているものになります。そのため閲覧数は一休の中でもかなりいいほうなのですが、受動的な情報ばかりなので残念ながら予約までたどり着く割合は低かったのです。

一方でメイン動線であるリストページは検索に特化しており、ユーザが能動的に欲しい情報を要求し、的確な情報を表示できます。そのため予約までたどり着く割合も高く、メイン動線としての役割をしっかりと全うしています。

この2つを統合することで流入数と予約割合のどちらも高めたいというのが狙いになります。

苦労したこと

キュレーションページというのは、「地域xテーマ」ごとにページが生成されており、施設はランキング形式で表示されています。(先に掲載した画像は各キュレーションページの親ページになります)

一方でリストページはさまざまな条件で検索ができるのですが、キュレーションページと同じように「地域、テーマ」による検索もできます。しかもランキング形式での表示もできるため、キュレーションページと同じようなページを再現できます。

しかしながら、同じ条件で検索してもキュレーションページに表示される内容と異なることがあったのです。

原因はランキング生成ロジックがそれぞれのページで異なっていたからです。

もちろんどちらも八百長ということはなくデータに基づいてランキングを生成しているのですが、細かい条件が少しずつ異なり、結果別の施設が表示されてしまったのです。

実装方針ですが、キュレーションページには親ページが存在し、親ページでもランキングを掲載しており、その親ページは残すという前提がありました。そのためリストページにキュレーションページのロジックを持ってくる必要があります。しかし一休ではアーキテクチャを更新している最中で、キュレーションページ側は旧アーキテクチャ、リストページ側は新アーキテクチャと別れてしまったためロジックの移植はしないことにしました。代わりにキュレーション側のアーキテクチャで「エリアIDとテーマID」を受け取ってランキングを返すAPIを作成し、それをリストページのアーキテクチャが叩くことにしました。

進捗は順調・・・かと思ったら・・・

大体のページではランキングの結果が一致するようになり開発は順調かと思われたのですが、一部のページに限ってランキングの結果が合わない状況に直面しました。

キュレーションページのURLをよくよく見てみると、なにやら見たことない数字がいました。

https://www.ikyu.com/theme/a140000/g10/101/

「a140000」は東京を表すエリアIDで、「101」というのがテーマIDになります。「g10」は何者だ・・・?

キュレーションページに詳しい方に聞いてみると、どうやらキュレーション関連のページ限定の概念であるジャンルIDをいうものもページ生成に影響しているようで、「地域xテーマ」ではなく「地域xテーマxジャンル」で生成されているとのことでした。

ここで困ったことが起きました。

APIの方は単にジャンルIDも指定できるようにすればよかったのですが、リストページではジャンルIDという概念がないため、そもそもAPIのパラメータに含めることができなかったのです。

ジャンルIDを無視することも検討したのですが、それだとキュレーション親ページに表示されている施設とリストページに表示されている施設が異なってしまう可能性があります。一休ではユーザファーストを掲げているため、そんな体験は許されません。

そこでリストページのアーキテクチャにジャンルIDという概念を組み込むことにしました。「リストページ」に組み込むのではなく、「アーキテクチャ」に組み込みます。このため当初予定していたよりも多くの改修が必要になり、リリース予定日も延長してしまいました。

ですが、最終的には要件を落とすことなく、ユーザ体験も悪化させずにリストページに統合することができました。先のリンクを開くとリダイレクトしてリストページが開くのですが、「g10」の部分を変えることで結果が変わることを確認できると思います。

統合を終えて

今回の統合では要件を落としたり、無理やり実装したりという部分もいくつかあったためちょっと悔しい思いも残っています。

しかし訪れた人が予約した割合を比較してみると、統合前に比べて約1.6倍ほどに増えていました。少しでも使い勝手が良くなったと思い、端的に嬉しかったです。

今後のプロジェクトでもより良い体験を提供できるよう精一杯努力して参ります。

hrmos.co