レバレジーズ株式会社 テクノロジー戦略室室長 兼 テラテイルチームリーダーの竹下です。
Architect New World on AWS 2022のイベントで発表した際のスライドです。 teratailのリプレイスのインフラ面に注目した発表になっています。
現在レバレジーズでは一緒に働いてくれる仲間を募集しています。ご興味のある方は、以下のリンクからご応募ください。
https://recruit.jobcan.jp/leverages/
レバレジーズ株式会社 テクノロジー戦略室室長 兼 テラテイルチームリーダーの竹下です。
Architect New World on AWS 2022のイベントで発表した際のスライドです。 teratailのリプレイスのインフラ面に注目した発表になっています。
現在レバレジーズでは一緒に働いてくれる仲間を募集しています。ご興味のある方は、以下のリンクからご応募ください。
https://recruit.jobcan.jp/leverages/
こんにちは。レバレジーズ株式会社の松本です。普段はHRテック事業部で主にフロントエンドの開発を担当しております。 今回の記事では、入社2ヶ月の私がテックフェス全体のレポートと、弊社フロントエンジニアの小林がReact18について発表した「Parce que j’aime la React.」を受けて、追加で調べたり試したりしたことをご紹介したいと思います!
レバレジーズグループに所属するエンジニアを対象に社内で半年に一度行われている技術の祭典です。エンジニアが新しい技術に興味を持ち、勉強をするきっかけを作ることを目的とし、グループ全体の技術力向上を目指します。
11月22日行われたテックフェス2022秋では、「まずは知ろう。次に学ぼう。そして明日使おう」というテーマで開催されました。
当日の流れは、有名な「テスト駆動開発」の日本語訳をされた和田卓人氏をお招きし、「質とスピード - ソフトウェア開発の典型的な誤解を解く(2022年秋版)」の基調講演から始まり、Sessionが2時間弱、LTが30分程、最後にパネルディスカッションが1時間程というタイムスケジュールでした。
オープニング
↓
基調講演(和田卓人氏)
↓
Sessions(30分 × 4名の弊社エンジニア)
↓
LT(5分 × 5名の弊社エンジニア)
↓
パネルディスカッション(1時間 4名の弊社ベテランエンジニア)
パネルディスカッションでは、「ITエンジニア35歳定年説について語る」というテーマで、意見を交わしていました。
・面白かった発表
特に面白かった発表は、個人的には和田卓人氏の発表と最後のパネルディスカッションでした。
和田卓人氏の「質とスピード」についての発表では、スピードのために犠牲にしようとしている品質とは何を指すのか、品質とスピードはトレードオフではないことを経験談やデータに基づいてお話ししていただきました。
スピードおよび質とトレードオフなのは教育、成長、多様性への投資であるというお話もあったので、内部品質(スピードは落とさない)への投資はエンジニアだけでなく事業会社全体にとって重要であると感じました。
また、最後のパネルディスカッションも個人的には非常に面白かったです。 一般的に、「ITエンジニア35歳定年説」というのはネガティブな意味で広まっていると思います。 調べてみると一般的に35歳定年説と言われているのは、学習能力の低下や体力の低下が理由とされているようです。
ただ、今回はそれとは反対にポジティブな印象を受けました。 登壇された方々もキャリアを積むにつれてコードを書くことが減ってきているとのことでしたが、前述したネガティブな理由ではなく、できることが増えたことで業務の幅が広がったことが大きな理由の一つとなっているようでした。 エンジニアに関わらず、年齢に関するトピックではネガティブに語られることが多いイメージだったので、今回はポジティブな話を聞くことができて非常に良かったなと感じました。
・関心があった発表
SessionsやLTの発表も非常に面白かったです。また、個人的には弊社フロントエンジニアの小林によるReactについての発表(30分のSesssion)「Parce que j’aime la React.」は、普段触れている技術領域ということもあり非常に関心がありました。
今回は、この発表から何を知って、何を学んで、何を使ったのかについても共有できればなと思います!
・知ったこと
小林の発表ではReact18で提供されるものとして、並行処理レンダリング、Suspense(正しくはアップデート)の紹介がありました。
例えば、以下のような実装だと、FetchComponent内でPromiseがthrowされている場合(サスペンドした場合)は、fallbackで指定されているComponentが返されます。このPromiseが解決された時はFetchComponentが返されます。
function App() { return ( <Suspense fallback={<Spinner />}> <FetchComponent /> </Suspense> ); }
また、Reactの日本語公式ドキュメントには、以下のような記述があります。
サスペンスにより、「UI ロード中状態」というものが、React のプログラミングモデルで宣言的に記述可能な主要コンセプトに昇格します。
今まではuseQuery等を利用してloadingの状態を実装者が管理し、それを用いてUIを分岐させるような実装をしていた方も多いと思います。私も同じような実装をしておりました。
この分岐のロジックをReact側が担ってくれるようになるため、我々はloadingの状態やそれを利用した分岐に関心を持つ必要がなくなり、ロード中とロード完了後のUIを宣言するだけで良くなったということだと解釈しています。
・学んだこと
React18で提供される機能の一つとしてTransitionというものがあります。今回はこの機能について学んでみました。
Transitionは更新処理に優先順位をつけたり、中断するための機能です。この機能では、更新処理を「緊急性の高い更新」と「トランジションによる更新(緊急性の低い更新)」で区別しています。緊急性の高い更新は即座にUIを更新します。一方、トランジションによる更新では緊急性の高い更新が発生したら更新処理を中断し、最終的な結果のみを返します。これはSuspenseと合わせて利用することもできるようです。
また、React18について学んでみて面白かったのは、この更新処理に優先度をつけたり更新処理を中断するような機能は ”React Fiber” というアーキテクチャによって実現可能になったということです。
Fiberについて調べていた中で、こちらの記事がとても面白かったです。 ”React Fiber”はReact16から採用されたもので、React16以前とは仮想DOMを更新してUIに反映するアルゴリズムが変更されているとのことです。今までは仮想DOMを上から再帰的に更新していたものが、各ReactElement一つ一つに対して更新処理が用意されるため、非同期的に仮想DOMを更新することが可能になったとのことみたいです。
また、少し古いものですが、こちらにも以下のような記載があります。
The goal of React Fiber is to increase its suitability for areas like animation, layout, and gestures.
“suitability”という単語が使われているように、FiberはUIの動きを絶対的に早くするものではなく適性を高めること、つまりUXの向上を目的にしていると考えられそうです。 この目的を達成するための基盤にFiberが必要で、今回のReact18で導入された機能はまさにこの目的を達成するための具体的な方法の一つと言えそうだなと感じました。
・使ったこと
今回学んだ内容は業務ですぐに使える内容ではないこともあり、ここではサンプルを触ってみた感想のみにしようと思います。公式のReact Docsに載っているuseTransitionのサンプルコードを手元で動かしてみました。
所感としてはSuspenseは比較的理解しやすく感じた一方で、useTransitionは思っていたよりも腰を据えて学んでいく必要があるものだなと感じました。
SuspenseもuseTransitionも背景には並行処理レンダリングがあって、私たちが見えている画面の状態とは別でもう一つ状態をもっていることが少し理解できました。 今後遠くない未来で実務での利用もありそうなので、引き続きインプットを続けていこうと思います。
今回の記事では、テックフェスで知った内容からReact18について自分なりに学んだことを書かせていただきました。ReactはUX改善のためのアップデートを続けているというのが非常に面白かったです。また、テックフェスは非常に魅力的な発表ばかりで、話を聞いていてとても楽しかったです。 私は今年の9月に中途で入社したばかりですが、このように組織内での技術の共有があるのは、ナレッジ共有の観点からも非常に有意義だと感じています。 皆さんもレバレジーズで一緒に働いてみませんか? レバレジーズに少しでも興味を持っていただけた方は、こちらからエントリーをお願いします!
こんにちは、テクノロジー戦略室の新城です。私は2022年6月に中途入社し、入社してまだ半年ですがMLOpsの推進という仕事を担当しています。
現在レバレジーズではレバテックダイレクトの求人マッチ度判定のような、レコメンドシステムを多くのサービスで活用しています。
一方で機械学習モデルを本番運用したはいいが、データサイエンティストがモデルの改善フローを回しにくく、再トレーニングしたモデルのデプロイが頻繁にできないといった問題が顕在化してきました。
そこで、今回は社内営業支援ツールにおいてMLOpsを導入し実際にデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介できればと思います。
レバレジーズのMLシステムはMLOps レベル0と言われる状態に近く、データサイエンティストがモデルを作成し、プロダクトへモデルを一度組み込むとその後の改善は行いにくく、プロダクトでのモデル品質も充分に確認できない状態でした。 あるプロダクトでは、図中のModel Servingの部分でプロダクションエンジニアがプロダクトの要求レベルに沿うように前処理の高速化等を行っており、トレーニング時と推論時に前処理が異なっている状態(Training-Serving Skewが起こっている状態)でモデルの更新に双方のエンジニアに工数がかかってしまう状況でした。 このような状況では、データサイエンティストは他のエンジニアの手を借りないとデプロイが出来ないという制約から、軽微なモデルの改善を手軽に行うことができません。加えて、レコメンドにおけるMLモデルはその品質を保つために、定期的に再学習を行い、トレンドを学習する必要があります。しかし、再学習時のデプロイ毎に双方のエンジニアに工数がかかる状態は好ましくありません。 そこで今回はデータサイエンティストが主導してモデルの改善・デプロイを行うことができる環境を整えることで、プロダクトエンジニアの工数を削減し、モデルの改善に積極的に取り組めるような仕組みづくりを目指しました。
上記の状態を改善するため、今回AWS SageMakerやGCP Vertex AIに代表されるようなMLプラットフォームの導入を検討しSageMakerを採用することになりました。データの分析から学習、モデルデプロイを一手に担ってくれるプラットフォームを導入することでデータサイエンティスト主導の機械学習モデル開発・デプロイができるようになることが狙いです。 またMLパイプラインを用途ごとに設けることでMLOps レベル1の状態に少しでも近づけることを目標としています。
今回はAWS SageMakerとStep Functions DataScience SDKを用いてMLパイプラインを構築しました。グレーアウトしている部分は今後の実装を予定している部分です。 図中の棒人間がいる箇所をデータサイエンティストに実装してもらい、後はパイプラインを実行していけばプロダクトに使用する図中左下のエンドポイントまでデプロイが完了します。 全体構成だけではわかりにくいと思うので各パイプラインの役割をそれぞれ見ていければと思います。
まずはデータ取り込み処理部分について順を追って説明をしていきます。
1.1. Event Bridgeの起動 学習・推論に使用するデータをBigQueryから定期的に読み込むためにEvent BrigeでStep Functionsの起動をしています。
1.2. データ取得と前処理 レバレジーズではデータウェアハウスとしてBigQueryを採用しており、データサイエンティストはここから任意のデータを取得・前処理・データフレーム化して使用します。 少しややこしいのですが、ここでの前処理と後述する学習パイプラインの前処理は異なっており、統計的な特徴量(平均値や分散値など)等の推論毎に作成する必要のない特徴量を予め作成し、推論時にこれらのDataFrameを予め読み込んでおくことで推論処理の高速化を図っています。
1.3. データの品質チェック(将来実装) 取得するデータの特徴量ドリフト検知といった品質チェックや定期実行毎の特徴量の推移等を出力できるとデータサイエンティストに気づきを与えることができるのではないかと検討中です。
S3に用意されたデータを元に学習から評価までを行っています。Lambda Stepではモデルとその評価値をSageMaker Model Registryへ登録しています。 ここでの処理は一般的に紹介されている学習パイプラインとほぼ同等と思って頂けたらと思います。
ModelRegistryへ登録されたモデルをデプロイするパイプラインです。
3.1. Model情報取得(Lambda) Step Functionsへの入力となるモデルパッケージ情報とバージョン情報を元にModel Registryから必要な情報を取り出しています
3.2. Endpoint新規作成・更新判定 このLambdaは単にEndpointの新規作成か更新かを判定するものです。入力されたEndpointNameを元に判定しています。
3.3. 推論Endpointの立ち上げ Endpoint立ち上げ時に前述のデータ取り込みパイプラインで作成したDataFrameを読み込んでおり、推論時に都度作成する必要のない特徴量はこちらを活用しています。 またデータキャプチャ設定で保存された推論結果をS3からBigQueryへ日次で転送しておりデータサイエンティストが推論結果をBigQuery上で解析できるようにしています。
3.4. エンドポイントに対しての自動テスト(未実装) データサイエンティストがプロダクトに関わるエンドポイントの実装とデプロイを行うということで、心理的安全性の担保ためにここの実装は必要だと考えています。
データサイエンティストが用意したパイプライン実行用のPython Notebookを使用して、モデルの改善を任意のタイミングでより容易に行うことができました。実際にSageMakerを導入してから短期間で複数回モデル変更のデプロイをデータサイエンティスト主導で行っておりその効果はあったのかなと思います。 一方で実行が手動なのは変わりなく、どのファイルを変更したらどのパイプラインを実行すれば良いといったフローはかなり煩雑なので、現在は変更に対応するパイプラインが自動実行されるといった自動化に着手中です。
こちらのルールの通り、コードを再利用する実装を事前にデータサイエンティストと進めたことで、基本的にはデータサイエンティストの変更のみでモデルのデプロイを行う仕組みを整えることができました。一方でエンドポイントのレスポンス速度は常に保証する必要があるため、前述した自動テストでのチェックや後述の前処理高速化に取り組んでいきたいと考えています。
今回はリアルタイム推論だったのですが、比較的前処理の量も少なく、特に工夫せずともプロダクト要件を満たす処理時間に収めることができたため学習コストの観点から導入はしませんでした。 但し、今後時間のかかる前処理やストリーミングで捌かないといけないデータが必要となった際にはApachBeamといったバッチとストリーム処理が可能なデータ処理パイプラインを導入しなければいけないと考えています。導入の際には推論時のみだけでなく、学習側もきちんと共通化できるように考えて実装していきたいです。
S3へ特徴量データを保存している部分をFeature Storeに置き換えたかったのですが、定期実行時にFeature Storeへ書き込むと今回のデータ量では書き込み量が多く、想定以上の料金がかかってしまうことがわかりました。データの差分のみを登録する仕組みを作る案も出ましたが、定期実行ごとにDataFrameを作りバージョン管理できた方が便利なため今回は採用を見送っています。上記の前処理高速化と関連してストリーミングデータを扱う際にはまた必要だと考えています。
MLOps レベル1への移行やより良いモデル開発体験を目指して以下のことを行っていきたいです。
今回の記事ではSageMakerを導入しデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介しました。MLOpsの導入と言い切るにはまだまだ改善すべきことが多いですが、まずは初めの一歩を踏み出すことができたのかなと思います。
また、これらの実装には入社してまだ半年の私を支えてくれた、データ戦略室のデータサイエンティストの理解と協力無くしては実現できませんでした。 このように、レバレジーズには年次や経験を問わず、主体的に取り組める環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!
レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。レバレジーズでは現在SRE(Site Reliability Engineering)チームを立ち上げ、全社を巻き込んでSRE化を推進しています。
SREを提唱したGoogleを筆頭に、日本でもメルカリやサイバーエージェントなど数多くのテック企業がSREチームを立ち上げ、インフラ基盤を維持管理するだけではなく、サイトの信頼性を担保することでサービス価値を最大化しようとしています。レバレジーズでも、1年ほど前からSREチームを立ち上げ、インフラからもお客様に最高のサービスを届ける事を目指しています。
この記事ではSREチーム立ち上げの背景と、レバレジーズがどのように顧客に寄り添ったSREチームを目指しているか、そしてSREで使用している技術のご紹介をしていきたいと思います。
レバレジーズ株式会社では、チームをサービス単位で分けており、それぞれが大きな裁量を持ってサービス運営をしています。 これまでは、チーム毎にインフラ担当者を置きサービスに合わせた構築、運用を行ってきていました。そのため、チームによって使用技術やインフラ構成が異なっており、チームの統廃合や異動、退職などによりインフラ担当が変わる際には、高い学習コストがかかっていました。また、担当者に寄ってインフラのレベルもまちまちなので、CI/CDやIaCを組み込んで自動化出来ているチームもあれば、手作業でインフラ変更をしているためインフラ作業で手一杯になっているチームもあったりしました。このままでは、今後事業分野も拡大してより多くのチームが出来た時に、チーム間の流動性と、全体の効率が低下してしまい組織規模の拡大のブレーキとなることが予期され、また、レバレジーズではマイクロサービス化を進めておりその観点からもインフラ周りの効率化が必要となってきていました。
そこで、インフラ関連の知識を集約し標準化を図るとともに全体最適を行うため、プロジェクト横断チームとして、SREチームを立ち上げることになりました。
SREチームのミッションとして
SRE化を通して、Developer Experienceの改善、事業の拡大への対応、お客様に信頼されるサービスの提供を実現する
を掲げています。
ただのインフラ屋としてタスクをこなすだけでは、レバレジーズのミッションである「関係者全員の幸福を追求する」ことの達成は難しく、また、インフラは顧客から少し離れた所で開発を行うため、誰のためのサービスなのかの意識が薄くなりがちです。そのため弊社のSREチームでは、インフラの上で動くサービスの更にその先に顧客がいることを忘れず、常にチームとしても個人としても成長を続けられることを目指し上記のミッションを設定しています。
上記のミッションを達成するにあたってどのようなSREチームを構築すればよいか調べましたが、いくつかの型が紹介されていたり、他社事例を調べても各社それぞれSREチームの形は違っていました。そのため、レバレジーズの文化にもっとも適したSREの形を模索し、Embedded SREとEvangelist SREのハイブリッドを目指すことにしました。
レバレジーズのエンジニアの使命は、世の中に価値あるサービスを届けるところにあると考えています。最大限の価値を届けるためには、Embedded SREは、ドメイン知識を持ち事業を理解した人材でないといけません。そのため、Embedded SREは開発チーム内のアプリエンジニアから選出してもらい、サービスのインフラ運営、監視、トイル撲滅、SLOの策定など、SREの実務を行いDevOpsの実現をしてもらいたいと考えています。
しかし、Embedded SREだけではチーム内に知識が閉じてこれまでと同様に局所最適に陥ってしまう可能性もあります。また、アプリエンジニアに任せることになるため技術レベルもチーム毎に違って来てしまいます。そこを補うためにEvengelist SREを置き、SREプラットフォームの構築、SREのトレーニング、ノウハウやベストプラクティスを各チームから吸い上げ、全社に浸透させるなどの動きをしていきます。今回立ち上げたSREチームは、このEvangelist SREの集団になります。
SREチームで標準となる技術を策定し全社に広めることで、ノウハウやベストプラクティスの共有を効率的に行おうとしています。現在使っている or これから使おうと考えている主要な技術を紹介したいと思います。
AWSでの新規のインフラ構築には主にCDKを利用しています。既存のリソースのインポートだったり、DataDogやGCPの場合はTerraformを利用しています。新しく立ち上げるサービスやリプレース時には、CDK,Terraformだけで構築できるようにしていますが、既存のEC2などでセットアップが必要な場合にはAnsibleを利用しています。
メトリックスやログは現在Datadogへ集約を進めています。また、DatadogもダッシュボードをIaC化し、全てのサービスで標準的な監視をすぐに始められるようにしています。またDatadog RUMやAPMを導入により、より詳細なメトリックスの取得し、効率的なパフォーマンスチューニングを目指しています。
新しく立ち上げるサービスは全てサーバーレス化を進めています。SREチームの前身になるインフラチームの際には人数が少なくkubernetesクラスターの維持管理が難しと考えて、フルマネージドサービスである、AWS Fargateを採用しています。GCPではCloud Runを主に採用しています。ただ、SREチームの人数も増え管理を、より自由な構成を行うためにkubernetesの導入検討も続けてはいます。
サーバレス化とともにマイクロサービス化も進めています。サーバー間の通信はgRPCを主に採用しており、一部のサービスではGraphQLも利用しています。また、サービス数も増えてきたため現在はAWSのフルマネージドのService MeshであるAWS App Meshの導入も進めています。
いかがでしたでしょうか?SREチームもシステムのその先にいるユーザーを視野に入れ、より良いユーザー体験を届けるために日々奮闘しています。また、ただ闇雲に頑張るのではなく、適切な技術を選定しかつ組織に浸透させるところまでを考えながらSRE化を進めています。
一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになりたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。
一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになって行きたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。 https://recruit.jobcan.jp/leverages/
カバー画像にもしているのですが、AIに「Site Reliability Engineering」で描いてもらったらなんとなくそれっぽいのになりました
初めまして、レバレジーズ株式会社の小林といいます。
私は2022年4月に開発未経験でエンジニアとして中途入社し、teratailというサービスのフロントエンド開発とマーケティング周りの業務に携わっています。
teratailでは、昨年末にリプレイスを行ったこともあり分析基盤がきちんと整備されておらず、各種分析ツール(Google Analytics4やBigQuery、Google Optimizeなど)を導入する必要がありました。
中でも今回は、導入に特に苦労した「Next.jsのServer Side RenderingでABテストを実装した方法」について紹介したいと思います。
今回の実装で使用する主な技術は以下の通りです。
参考文献が少なかった
通常のReactアプリ(SPA)のOptimize導入については参考文献が豊富ですが、Next.jsのServer Side Rendering時などにサーバー側からABテストを制御する方法に関しては文献が多くありませんでした。公式リファレンスを読んでも「???」な部分が多くあり、予想以上に詰まったため、同じような苦労をされている方は多いのではないかと思い記事にしました。
今回の苦労を残しておきたかった
このタスクを担当したときは入社3〜4ヶ月目の頃でしたが、開発経験ほぼゼロで入社した私にとっては難易度が高かったです。入社早々に今回のような貴重な経験ができたので、この苦労をここぞとばかりに共有したく、記事にしました。
プロダクト自体は月間PV数が1,000万を超えており十分なデータが存在するにも関わらず、分析基盤が整っていないために、データドリブンに施策の立案や効果の検証が行えていないという課題がありました。 今後teratailが更にグロースしていく上で、データドリブンに意思決定を行える体制を作ることが必須だと考え、そのための分析基盤構築の一環としてABテスト環境を整えることとなりました。
特に以下のような方々の参考となれば嬉しいです。
プリレンダリング(ページにアクセスがあったときにサーバー側でJavaScriptを実行してHTMLを生成する)機能の一つで、SEO対策や高速なコンテンツ表示が可能となります。
Google Optimizeとは
Googleが提供する無償のABテストツールです。専用のタグをサイトに埋め込むだけで簡単にエクスペリエンス(=ABテスト)を実施することできます。エクスペリエンスやデータの集計は_gaexpというcookieで管理されます。
管理画面でエクスペリエンスの実施が可能
Google Optimizeの特徴として、ブラウザの管理画面で各種設定を行うことで簡単にABテストを実施することができる、という点が挙げられます。ノーコードでテストパターンの要素変更ができるため、非エンジニアでも気軽に操作することが可能です。
通常、Google OptimizeのABテストはクライアント側で処理される仕様のため、サーバー側で機能を切り替えることができません。クライアント側でページの読み込み後に処理されるため、ABテストの実行(要素の変更)時にはどうしてもちらつき(ページフリック)が発生してしまいます。
今回、このちらつきを避けることや、管理画面側では設定できないようなABテストをプログラム側で実装することを考慮し、通常の方法ではなくサーバー側でOptimizeを使用する環境を整える必要がありました。
それではここから、本題の「Server Side RenderingとGoogle OptimizeでABテストを実装する方法」について説明していきます。
Server Side RenderingでGoogleOptimizeのABテストを実装するためには、Optimizeのサーバーサイドテストの機能を使い、プログラム側とOptimize管理画面側でそれぞれ以下の項目を実装・設定する必要があります。
【プログラム側での実装】
1) テストパターンの割り当て
2) Optimize(GA)側にテストパターンを送信
3) テストパターンのグローバル管理、取得
4) コンテンツの出しわけ
【Optimize管理画面側での設定】
1)テストパターンの作成
2)ページターゲティング
3)その他
それぞれ説明していきます。
1) テストパターンの割り当て
まず、ABテストのseed値がCookieにあればCookieから取得、なければ生成する関数を定義します。
(※seed値にはuserIdを用いてもOKですが、今後「ログアウト⇨ログイン状態」をまたいだABテストを行うことも考慮し、userIdを利用せずにABテスト用にseed値を生成してパターンの振り分けを行うように実装しています。)
【abTestSeed.ts】
import { NextPageContext } from 'next'; import nookies, { parseCookies } from 'nookies'; export interface AbTestSeed { seed: string; } export const COOKIE_KEY_AB_TEST_SEED = 'abTestSeed'; const genRandomString = () => { return Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2); }; export const getOrCreateAbTestSeed = async (context: Pick<NextPageContext, 'req' | 'res'>): Promise<AbTestSeed> => { const cookies = parseCookies(context); if (COOKIE_KEY_AB_TEST_SEED in cookies && cookies[COOKIE_KEY_AB_TEST_SEED]) { return { seed: cookies[COOKIE_KEY_AB_TEST_SEED], }; } const seed = genRandomString(); nookies.set(context, COOKIE_KEY_AB_TEST_SEED, seed, { maxAge: 5 * 365, path: '/', httpOnly: false, sameSite: 'lax', }); return { seed, }; };
次に、今回のエクスペリエンスをSAMPLE_TESTとし、以下の項目を設定します。
【abTestSetting.ts】
export interface AbTestSetting { experimentId: string; // OptimizeのエクスペリエンスID triggerPaths?: RegExp[]; // テスト開始をトリガーするPathを指定 numOfVariants?: number; // テストパターンの数 targetRatio?: number; // 対象とするユーザーの割合。0~1で設定 } // 全てのクエリを含む、トップページが対象 const TopPageRegex = /^\/(\?.*)?$/; export const SAMPLE_TEST = { experimentId: 'XXXXXXXXXXXXXXXX', numOfVariants: 2, targetRatio: 1, triggerPaths: TopPageRegex; }; export const ABTests: AbTestSetting[] = [SAMPLE_TEST];
今後複数のエクスペリエンスを実行することを考慮し、ABTestsという変数に配列で格納しておきます。
次に、abTestSeedを受け取ってテストパターンを返す関数を定義します。
OptimizeABテスト用のReact Contextも作成しておきます(詳細は後述します)。
【abTestState.ts】
import { createContext } from 'react'; import { ABTests, AbTestSetting } from './ABTestSettings'; import * as crypto from 'crypto'; import * as hashSha256 from 'sha256-uint8array'; export type ABTestVariant = { experimentId: string; variant: number; }; // テストパターンを決定する export const determineVariantsForPath = (path: string, seed: string): ABTestVariant[] => { const abTests = ABTests.filter((setting) => { if (!setting.triggerPaths) { return true; } return setting.triggerPaths.some((pattern) => pattern.test(path)); }); return _determinVariants(abTests, seed); }; const _determinVariants = (abTests: AbTestSetting[], seed: string): ABTestVariant[] => { const variants = abTests .map((setting) => { return { experimentId: setting.experimentId, variant: determineVariant(setting, seed), }; }) .filter((variant) => variant.variant !== NO_VARIANTS); return variants; }; const UINT_MAX = 4294967295; export const NO_VARIANTS = -1; export const determineVariant = (setting: AbTestSetting, seed: string) => { //Seed値をハッシュ化させてパターンを割り振る const digest = hashSha256.createHash().update(`${setting .experimentId}-${seed}`).digest().buffer; const patternBase = new DataView(digest.slice(0, 4)).getUint32(0); const numOfPatterns = setting.numOfVariants || 2; const targetRatio = setting.targetRatio || 1; const pattern = patternBase % numOfPatterns; if (targetRatio >= 1) { return pattern; } else { // 比率が1ではない場合は、4~8byte目を使用する const ratioBase = new DataView(digest.slice(4, 8)).getUint32(0) / UINT_MAX; if (ratioBase < targetRatio) { return pattern; } else { return NO_VARIANTS; } } }; //グローバルで状態管理するためにReact Contextを使用 export const ABTestContext = createContext({ seed: 'empty', }); //テストパターンを取得。実際にABテストを実施するコンポーネントで使用する export const getVariant = (setting: AbTestSetting): number => { const seed = useContext(ABTestContext).seed; const variant = determineVariant(setting, seed); if (variant !== NO_VARIANTS) { return variant; } else { return undefined; } };
2) Optimizeにテストパターンを送信
abTestSeedを取得する関数を_app.tsx内で実行し、メタ要素を管理するMetadataコンポーネントに渡します。
【_app.tsx】
import { NextPageContext } from 'next'; import { useRouter } from 'next/router'; import Metadata from './Metadata'; import { determineVariantsForPath } from './ABTestState'; const App = ({ Component, pageProps, abTestSeed }) => { const router = useRouter(); return ( <> <Metadata abTestVariants={determineVariantsForPath(router.asPath, abTestSeed)} /> <Component {...pageProps} /> </> ); }; App.getInitialProps = async (appContext: AppContext) => { const abTestSeed = await getOrCreateAbTestSeed(appContext.ctx); return { abTestSeed: abTestSeed.seed }; }; export default App;
【Metadata.tsx】
import React from 'react'; import Script from 'next/script'; import { ABTestVariant } from './ABTestState'; const googleAnalyticsTrackingId = 'YYYYYYYYYYYYYYY' const googleOptimizeTrackingId = 'ZZZZZZZZZZZZZZZ' interface MetadataProps { abTestVariants: ABTestVariant[]; } const Metadata: React.FC<MetadataProps> = (props) => { return ( <> <Script src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsTrackingId}`} /> <Script id="gtag-snippet" strategy="afterInteractive" dangerouslySetInnerHTML={{ __html: ` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${googleAnalyticsTrackingId}', { page_path: window.location.pathname, experiments: [${props.abTestVariants .map((variant) => { return `{ id:'${variant.experimentId}', variant:'${variant.variant}' }`; }) .join(',')}], }); `, }} /> </> ); }; export default Metadata;
これで、トップページにアクセスがあったタイミングでOptimizeにデータが送信されるようになります。
3) テストパターンのグローバル管理、取得
(2)で記述した_app.tsxのコンポーネントをReact Contextでラップすることで、テストパターンをグローバルで状態管理します。
【_app.tsx】
import { ABTestContext} from './ABTestState'; //...省略... return ( <ABTestContext.Provider value={{ seed: abTestSeed }}> <GoogleAnalyticsMetadata abTestVariants={determineVariant(abTestSeed)} /> <Component {...pageProps} /> </ABTestContext.Provider> ); }; //...省略...
私たちのプロダクトでは状態管理にRecoilを使用していますが、Recoilの場合、RecoilRootごと(各ページごと)に状態が分割しているためテストパターンを全体で共有できず、React Contextを使用するようにしました。
4) コンテンツの出し分け
最後は実際の表示部分です。
テストパターンを取得し、表示を出しわけます。
【SampleComponent.tsx】
import React from 'react'; import { getVariant } from './ABTestState'; import { SAMPLE_TEST } from './ABTestSettings'; const pattern = ['オリジナル','パターンA'] const SampleComponent: React.FC = (props) => { const variant = getVariant(SAMPLE_TEST); return ( <> <p>{pattern[variant]}</> <> ); }; export default SampleComponent;
上記のコンポーネントでは、テストパターンが0だと「オリジナル」、1だと「パターン1」が表示されるようになります。
※実施するABテストの内容によっては、コンポーネント自体を別で作って表示を出し分けるような実装でもいいかと思います。テスト内容に応じて調整ください。
以上がプログラム側で必要な実装となります。
Optimize側で必要な設定は主に以下の2つです。
1) テストパターンの作成
ABテストに必要な数だけパターンを作成します。
テストの実装は全てプログラム側で行っているため、編集ボタンでの操作は一切不要です。パターンを作成するだけでOKです。
2)ページターゲティングの設定
存在しないサイトのURLを入力してください。正式なURLを入力してしまうとエクスペリエンスがOptimize側で自動的に開始されてしまい、プログラム側で実装した処理よりも優先して開始されてしまいます。ここでは公式リファレンスを参考にして「SERVER_SIDE」と入力しています。画像のようなWarningが出ますが、無視でOKです。
その他
それ以外に必要な設定は特にありません。管理画面にはトラフィックの割り当てやアクティベーションイベントなどを設定する項目がありますが、今回のサーバーサイドでのABテストの場合、それらは全てプログラム側で設定することになるため管理画面側での設定は不要です。
あとは、GAとの連携やOptimizeのインストールが済んでいればエクスペリエンスの開始が可能なので、「開始」ボタンをクリックしてエクスペリエンスを開始させてください。プログラム側からデータが正常に送られているとデータが集計されます。
設定は以上となります。
gtag.jsでのデータ送信
Googleの測定サービスにデータを送信するにはgtag.jsというフレームワークを使いますが、公式のサンプルコードではgtag.jsの’set’ディレクティブが記述されているものの、今回の実装だとデータが送信されませんでした。結局’config’ディレクティブに変更して上手くいきましたが、かなり詰まりました。
管理画面側で必要な設定項目
通常のReactアプリ(SPA)でのOptimize実装の文献を読むと「アクティベーションイベントを設定してプログラム側でuseEffectで呼び出す〜」といった説明がありますが、サーバーサイドでのテストの場合はこれらの設定は一切不要です。
上述の通り、管理画面側で必要な設定は主に2つのみです。管理画面はあくまでABテストを行うためだけの土台作りのイメージ、ということを認識する必要がありました。
状態管理
状態管理にはRecoilを使用していますが、私たちのプロダクトでは各ページごとにRecoil Rootで状態を分割しているため、テストパターンの値を全体で共有できませんでした。React Contextを使うことで解決しました。
ABTestSeed生成のタイミング
該当ページにアクセスした時にseed値を生成するようにすると、複数ページにまたがるABテストを実行する場合に矛盾が生じてしまいます。
例)/hogeと/fugaで同一のテストを行いたい場合、/hogeにアクセス(オリジナル) ⇨ /fugaに遷移(テスト発火) ⇨ /hogeにアクセス(パターンA) のように、同じユーザーに対して/hogeの表示内容が変わってしまう可能性があります。
なのでABTestSeedの生成を_app.tsxで実行することで、全てのページにアクセスがあったタイミングでseed値の生成をするようにしています。
いかがでしたでしょうか。
Server Side RenderingでのABテストの実装は参考文献が少なく、詰まった部分がとても多かったですが、今回無事に実装できてよかったです。これでABテストを行う基盤が整ったので、今後は実際にテストを沢山まわしてプロダクトの分析・グロースに繋げていければと思います。
レバレジーズでは、私のようにエンジニア未経験で入社した社員でも幅広くチャレンジできる環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!
const Component = () => {
const [state, setState] = useState(true);
const data = fetch('https://api.endpoint');
// ...
// ここから上がロジック
return (
<>
<h1>{data.title}</h1>
<!-- UI表示の分岐ロジック -->
<p>{state ? 'on' : 'off' }</p>
</>
)
}
一言でロジックと言っても、以下の2種類に分類できると考えています。
const Toggle = ({ isActive }) => {
return isActive ? <ActiveToggle/> : <InActiveToggle/>
}
UIロジックは、「業務に依存しない」純粋なUIのふるまいの制御になります。
const Price = () => {
const { data } = fetch('https://api.endpoint');
const { quantity, unitPrice ,taxRate, shippingCost } = data;
const basePrice = quantity * unitPrice;
const price = (basePrice + shippingCost) * taxRate;
return (
<>
<h1>価格: {price}円</h1>
<p>内訳</p>
<ul>
<li>個数: {quantity}個</li>
<li>単価: {unitPrice}円</li>
<li>税率: {taxRate}%</li>
<li>送料: {shippingCost}円</li>
</ul>
</>
)
}
// アクティブなラジオボタン
const ActiveRadio = () => {
return (
<div style={{
width: 15px;
height: 15px;
backgroundColor: white;
border: 1px solid black;
borderRadius: 10px;
display: flex;
alignItems: center;
justifyContent: center;
}}>
<span style={{
width: 5px;
height: 5px;
backgroundColor: blue;
borderRadius: 10px;
}} />
</div>
)
}
const Title = ({ text }) => {
return <h1 style={{ fontSize: 20px }}>{text}</h1>
}
return
の上にロジックが書かれることはありません。import { ActiveRadio, InActiveRadio } from '~/atoms/radio';
// on/offができるラジオボタン
const Radio = ({ isActive }) => {
return isActive ? <ActiveRadio /> : <InActiveRadio />
}
import { Button } from '~/atoms/button';
// ファイルアップロードフォーム & ボタン
const FileUpload = ({ name, text }) => {
const ref = useRef(null);
const onClickInput = () => ref.current.click();
return (
<>
<Button onClick={onClickInput} >{text}</Button>
<input hidden name={name} ref={ref} type="file" />
</>
);
}
Custom hook
で共通化したとします。// usePulldown.ts
export const usePulldown = () => {
return ...
}
一般的なプルダウン
// Pulldown.tsx
const Pulldown = () => {
const pulldownProps = usePulldown()
return <Pulldown {...plldownProps}/>
}
検索可能なプルダウン
// SearchPulldown.tsx
const SearchPulldown = () => {
const pulldownProps = usePulldown()
// ... SearchPulldown独自のロジック
return (
<>
<!-- other component -->
<Pulldown {...plldownProps}/>
</>
)
}
複数選択可能なプルダウン
// MultiPulldown.tsx
const MultiPulldown = () => {
const pulldownProps = usePulldown()
// ... MultiPulldown独自のロジック
return (
<>
<!-- other component -->
<Pulldown {...plldownProps}/>
</>
)
}
ここでusePulldown
の処理の中でSearchPulldown.tsx
のみに例外的な処理の追加が必要になったとします。usePulldown
の中から例外的な処理が入る部分を切り出して他のPulldown
やMultiPulldown
にも変更箇所のコードを移植することになるかと思います。if
文を一行入れて例外的な処理を走らせるような直感的な対処をしてしまいます。// usePulldown.ts
export const usePulldown = (type) => {
+ if(type === 'searchPulldown') doSomething();
return ...
}
依存される側のusePulldown
が依存する側のコンポーネントの情報を保持することになります。import { Button } from '~/atoms/button';
// ファイルアップロードフォーム & ボタン
const FileUpload = ({ name, text }) => {
const ref = useRef(null);
const onClickInput = () => ref.current.click();
return (
<>
<Button onClick={onClickInput} >{text}</Button>
<input hidden name={name} ref={ref} type="file" />
</>
);
}
逆に特定の業務ドメインに縛られているとは以下のような状態です。import { Button } from '~/atoms/button';
// ファイルアップロードフォーム & ボタン
const FileUpload = ({ name }) => {
const ref = useRef(null);
const onClickInput = () => ref.current.click();
return (
<>
<Button onClick={onClickInput} >プロフィール画像のアップロード</Button>
<input hidden name={name} ref={ref} type="file" />
</>
);
}
どんなシステムを開発しているかの前提もないですが、
上記のファイルアップロードボタンはユーザ情報の登録や編集を担う画面でしか活用できないコンポーネントになることが想像できます。import { Button } from '~/atoms/button';
// ファイルアップロードフォーム & ボタン
const FileUpload = ({ name }) => {
const ref = useRef(null);
const onClickInput = () => ref.current.click();
return (
<>
<Button onClick={onClickInput} >商品画像のアップロード</Button>
<input hidden name={name} ref={ref} type="file" />
</>
);
}
ただシステムが以下のような構成だと上記のコンポーネントはmoleculesとして成立しません。
import { Title, Paragraph } from '~/atoms'
import { PriceList } from '~/molecules/PriceList'
// 価格
const Price = () => {
const { data } = fetch('https://api.endpoint')
const { quantity, unitPrice ,taxRate, shippingCost } = data;
const basePrice = quantity * unitPrice;
const price = (basePrice + shippingCost) * taxRate;
return (
<>
<Title>価格: {price}円</Title>
<Paragraph>内訳</Paragraph>
<PriceList
quantity={quantity}
taxRate={taxRate}
shippingCost={shippingCost}
/>
</>
)
}
import { Title } from '~/atoms/Title'
import { PriceList } from '~/molecules/PriceList'
// 価格
const Price = () => {
const { data } = fetch('https://api.endpoint')
- const { quantity, unitPrice ,taxRate, shippingCost } = data
+ const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data
cons t basePrice = quantity cons
unitPrice t price = (basePrice + shippingCost)
taxRate
return (
<>
- <Title>価格: {price}円</Title>
+ <Title>価格: {isMember ? price * 0.9 : price}
<p>内訳</p>
<PriceList
quantity={quantity}
taxRate={taxRate}
shippingCost={shippingCost}
/>
</>
)
}
この例ではコードの行数が短いですが、実際のorganismsコンポーネントになるとコードの行数が長くなりがちで自然とこういった楽な実装に流されることが多いです。import { Title } from '~/atoms/Title'
import { PriceList } from '~/molecules/PriceList'
// 価格
const Price = () => {
const { data } = fetch('https://api.endpoint')
- const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data
+ const { quantity, unitPrice ,taxRate, shippingCost, isMember, isGoldMember } = data
cons t basePrice = quantity cons
unitPrice t price = (basePrice + shippingCost)
taxRate
return (
<>
- <Title>価格: {isMember ? price * 0.9 : price}
+ <Title>価格: {isGoldMember
+ ? price * 0.8
+ : isMember
+ ? price * 0.9
+ : price
+ }円
<p>内訳</p>
<PriceList
quantity={quantity}
taxRate={taxRate}
shippingCost={shippingCost}
/>
</>
)
}
元々のコードでDOMに三項演算子が実装されていると自然とそこに追加する実装をしてしまうことがあると思います。return
の上だけでなくDOMの中にも存在することになります。organisms/
├ Presentations/
│ └ Price.tsx (UIを責務としたコンポーネント)
│
└ Containers/
└ PriceContainer.tsx (業務ロジックを責務としたコンポーネント)
Presentational Components
Container Components
// organisms/Price.tsx
import { Title } from '~/atoms/Title'
import { PriceList } from '~/molecules/PriceList'
// 価格
const Price = ({
price,
quantity,
taxRate,
shippingCost
}) => {
return (
<>
<Title>価格: {price}円</Title>
<p>内訳</p>
<PriceList
quantity={quantity}
taxRate={taxRate}
shippingCost={shippingCost}
/>
</>
)
}
PresentationalCompornents
は業務ロジックを持たないため、storybook等のUIのライブラリ化も容易にできます。
propsが表示すべきデータの構造そのものになります。
// organisms/PriceContainer.tsx
const PriceContainer = () => {
const { data } = fetch('https://api.endpoint')
const { quantity, unitPrice ,taxRate, shippingCost } = data;
const basePrice = quantity * unitPrice;
const price = (basePrice + shippingCost) * taxRate;
return (
<Price
price={price}
quantity={quantity}
taxRate={taxRate}
shippingCost={shippingCost} />
)
}
PresentationalComponents
に委ねます。ContainerComponents
では位置の調整や余白のためにスタイルを当てることもありません。
PresentationalComponents
とContainerComponents
はディレクトリも区分けして明確に違うものとして定義しておくのが良いです。Presentational/Container Components
をどういった構造にすべきか設計することは難易度が非常に高いです。
まずはPresentational Components
をひたすら実装していき、画面まで実装するのが望ましいです。
その後APIをpagesコンポーネントで繋ぎこみます。
pagesでAPIを発行するとorganismsのPresentational Components
を辿って末端のatomsコンポーネントまでデータをバケツリレーしていくことになります。
この時にpropsとして受け取ったデータを無加工でそのまま更に末端のコンポーネントに渡す場面が発生するはずです。
その場面でContainer Components
の実装を検討していくというのが手戻りを少なくできます。
こんにちは、レバレジーズ株式会社エンジニアの住村です。 現在、私が開発に関わっているレバテックでは、IT人材向けのフリーランス事業を開始してから成長フェーズを経て、2022年現在では市場のトップシェアを獲得しています。
レバテックでは、今後も市場のトップシェアを取り続け、トップであることを活かしてエンジニア業界を主導して流れを作り、業界全体を良い方向に動かしていくことを目指しています。そのためには、エンジニアも目先の開発だけに囚われず、職種も超えて協力して事業を創っていく必要があります。
今回は、認証基盤システムの開発を通じて、エンジニアが今後の事業戦略を支えるためのシステム開発をどのように進めたかの事例を紹介していきたいと思います。
レバテックでは、フリーランス・正社員・未経験からのスタートなど、さまざまな形で職を探しているエンジニア・クリエイターと企業のマッチングを支援しています。
一度サービスを利用したら終わりではなく、プログラミングを学ぶフェーズ(カレッジ)から、就職(ルーキー)・転職(キャリア)といったサービスを継続的に受けることが可能です。過去の蓄積データを元にエンジニアはより充実したサービスを受けることができます。エンジニアをターゲットとしたサービスを提供している関係上、ユーザーの行動に合わせて当初とは異なるサービスを提供することもあります。
また、継続して顧客に利用してもらうことでファン化を促進し、顧客生涯価値(LTV)を高めるだけでなく、サービスの情報を知り合いのエンジニアに紹介してもらう、新規に展開する別サービスの潜在顧客になるなど、今後のマーケティング活動にも繋げることができます。 今回は、この「ユーザーに継続してサービスを使い続けてもらう」事業戦略を実現するために、根幹となる認証基盤システムを構築していきました。
前述の事業戦略の実現にあたり、1つのサービスを利用したユーザーがその後も別のサービスをシームレスに利用し続けるためには、システムに大きな課題が存在しました。
それは、これまでのレバテックでは各種サービスが使い切りのビジネスモデルであったため、システムもサービスの開始に合わせて都度構築していたことです。
その際、認証機能も新規システムを構築する度に作成し、そのシステム内でのみ管理しているため、ユーザーはレバテックの別のサービスを利用する度にアカウントを作成しなければなりませんでした。 また、同じ処理を別々のシステムで開発していたために無駄な開発コストがかかっていたのも事実です。それ以外にも、あるサービスではSNSログインが使えるが別のサービスではEmail/PWでしかログインできない、パスワードに使える文字列の長さや種類が異なる、といったように認証要件も統一されていませんでした。
今後の事業戦略を実現するため、シームレスにサービス間を利用できるようにするにはこれらの問題点も解消する必要がありました。 そのため、レバテックでは共通の認証基盤を作成し、既存サービスの認証方法を統一する判断に至りました。
今回のシステムを構築するにあたり、前述の事業戦略からサービス間を横断的にアクセスできる必要性があるため、まずサービス間でのSSOログインを実現する必要がありました。 SSOログインの実現にあたり、大枠の方針としてはAuth0やCognitoやFirebaseといった外部サービスを利用するか、各システムで認証プロセス管理するライブラリを用意するなどの自社で開発するかの2つが選択肢として挙がりました。 SSOログイン機能を開発するには大きく分けて認証プロセスの管理と認証情報の管理の2つの機能が必要です。
今回は認証プロセスに独自仕様がないことと、実装コストを削減するために外部サービスのALBの機能を利用しました。 その上で、認証情報の管理は既存システムからの移行やアカウントの統合といった独自仕様があったため、独自で実装する選択を取りました。 また、認証プロトコルにはALBでサポートされておりセキュリティ信頼性の高いOpenID Connect(OIDC)を採用しました。
今回の構成では、 ・クライアントのログインセッションの管理 ・セッションの状態に応じたログイン画面への振り分け ・認証成功時のアクセストークンの管理 ・認証が完了したセッションへのJWTの発行 などの認証プロセスの管理をALBへ任せています。
ALBを利用しない場合はこれらをサイトで管理する必要がありましたが、今回の構成ではALBが管理するため、共通認証基盤の利用に必要な対応が大幅に削減できます。 そのため、既存のサイトはJWTの有無で認証状態を、JWTを検証してどのユーザーがアクセスしているかチェックするだけで良くなります。認証の仕様はOpenID Providerが共通で持っているため、認証仕様を一元管理することで全てのサイトで認証のセキュリティレベルを統一できます。
他にも新規にサイトを構築する場合、共通の認証基盤を利用するにはALBの設定を追加すれば、後はJWTの検証の仕組みを入れるだけで済みます。 これにより、今後の事業展開でサービスを増やす場合のハードルを大きく下げることができました。
今回構築を行い、分かった点ですが、ALBを利用する場合は認証プロセスを15分以内に完了しない場合はALBから401エラーがクライアントに返却される仕様がありました。 これに関してはAWSの公式Docにも記載がありますが、このタイムアウトの期間は変更も削除もできない仕様になっています。 今回はアプリ側でタイムアウト期限を過ぎる場合の挙動を制御することで対応しましたが、ALBでタイムアウト時の遷移先が制御できると嬉しいですね。
今回の記事では、レバテックの今後の事業戦略と、実現に必要なSSOログインの認証基盤システムをどのように構築したかについて紹介しました。 レバレジーズでは、今回の事例のように短期的な開発だけでなく事業の長期戦略に必要なシステムをエンジニアが主導して進めることができます。 それは、事業の方向性議論にエンジニアも参画するためです。
レバレジーズでは、エンジニアとしてシステムの開発だけでなく、事業の未来を考えサービスを創っていくことに意欲のある仲間を募集中です!ご興味のある方は、以下のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
こんにちは、2022年度新卒でレバレジーズ株式会社に入社した河原です。
現在私は、若年層領域の事業を展開する『ハタラクティブ』や『キャリアチケット』というサービスで、営業職が使用するSFA開発*1に携わっています。 機能の多いSFAでの開発業務や業務・業界理解など難しいことが多いですが、営業職の社員が同じ職場にいて、日々私たちが開発したシステムを使って仕事しているところを見かけると、とてもやりがいを感じられます!
本記事では、先日行われたレバレジーズエンジニア組織テックフェスでのテックトークセッションの内容をご紹介します!また、私が従事しているSFA開発にどうすれば活かせるかを考え、クラウドコンテナサービスの調査・検証してみました。
テックフェスは、レバレジーズのエンジニア組織メンバー全員を対象としたテクノロジーの祭典です。弊社エンジニアの技術力を向上させ、より良いサービスを世の中に提供できるようにするために企画されました。
今回、記念すべき第一回目のテックフェスは、以下のようなタイムスケジュールで行われました。
今回は、テックフェス後半で行われたテックトークでの内容をご紹介したいと思います。
テックトークのコーナーでは、各事業部のシステム開発を行う開発チームリーダークラスのエンジニアが、開発を通じて得られた経験のふりかえりや技術の活用法についての話を5〜10分の時間でプレゼンしました。テックトークのタイトルは以下の通りです。
インフラリソースや関数型言語など技術にフォーカスした話から、事業部側と密に連携する開発体制についてなどレバレジーズの開発組織ならではの話もあり、短い時間ながらもとても役に立つ内容でした!
今回のテックトークに刺激を受け、私のチームのシステム開発に活かしてみたい!という気持ちが生まれました。私は様々なトークテーマの中から、現在の私の開発に活かせるものがないかを模索しました。
開発チームリーダーにSFA開発の現状を確認したところ、以下のようなインフラの構想があることがわかりました。
しかし、この環境構築の担当エンジニアは決まっておらず技術調査もあまり進んでいない状況でした。
そこで、テックトークでの『Cloud Runへのデプロイ自動化が簡単で脳汁が出てしまった話』の内容をさらに深ぼり、クラウドのコンテナリソースについて調査・検証してみました!
テックトークは主に以下のような内容でした。 Cloud Runを使えばコンテナの自動デプロイが簡単に行えるそうです。 コンテナはベンダーロックインされない特徴を持ち、昨今のマイクロサービスアーキテクチャの実現には不可欠な仮想化技術です。
本当に簡単にデプロイやCI/CDが設定できるのでしょうか?また、他にはどんなコンテナリソースがあり、それぞれの違いは何があるのでしょうか? その違いを知っておきたく思い、クラウドサービスを調査してみました!
テックトークの内容はGCPであり、普段の開発ではAWSを使っていることから、2つのクラウドサービスに対して調査を行います(ちなみに私はコンテナリソースに関してはほぼ無知な状態でした…)。調査した内容をカテゴリ分けすると以下のようになります。
上記のように、同じサーバーレスコンテナリソース同士にも細かな違いが存在するそうですが、それはどのような箇所なのでしょうか? 調査を一通り終え、それぞれのコンテナリソースの違いが大まかにわかってきたところでGCP・AWSのコンテナリソースを簡単に検証してみます!
今回は、以下の2つのクラウドサービスを用いて検証を行います。
GCPのCloud Runは今回のテックトークで登場し、その手軽さを実感したいと思ったため選択しました。AWSにはCloud Runに似たサービスとしてApp Runnerがあります。App Runnerの設定や機能はCloud Runとほぼ同様のため、コンテナリソース間の違いをあまり実感できないのではないかと思い、今回はApp Runnerより少し複雑な設定や調節が可能な『ECR + ECS + Fargate』を使って検証してみます。
また、今回はクラウドサービスの比較検証がメインであるため、WebサーバーであるNginxを使った簡単なコンテナ環境で動作確認を行います!下記構成でGithubに登録した状態で検証スタートです!
.├── Dockerfile └── html └── index.html
まずは、テックトークでも登場したCloud Runを使ってみます。GCPのコンソール上の簡単な操作で自動デプロイ設定が可能なようです。実際に試してみましょう!
GCPにProjectを作成し、Cloud RunとCloud BuildのAPIを有効にします。テックトークで説明された項目を作成したGithubのリポジトリで設定していきます。
次に作成するコンテナの設定をしていきます。今回は検証なのでユーザー認証の設定はせず、リクエストの処理中のみコンテナを起動する設定にしてみます。
設定が終わり、GCPがコンテナの準備を始めます。
作成されたコンテナはhttpsで自動にルーティングされます。 URLにアクセスするとGithubにpushしたコードがデプロイされていることが確認できました。本当に簡単ですね…。
次にCD(Continuous Deployment:継続的デプロイ)の設定をしていきます。 コンソールの「継続的デプロイの編集」を押し、以下のように編集していきます。今回はmasterブランチへpushされたイベントでコンテナがデプロイされる設定を行ってみました。
試しにコードに、簡単なcssと画像を追加し、Githubにpushしてみます。 すると、そのイベントを検知し、デプロイが始まります。
しばらくするとコンテナが起動し、変更を確認することができました。CDの設定まで非常に簡単にできてしまいます。
次は、AWSのコンテナ用サーバーレス環境のFargateを使って、ECRとECSでコンテナをデプロイしてみます。
ECSはフルマネージド型コンテナオーケストレーションサービスと呼ばれており、Cloud Runよりも設定や導入は複雑ですが、その分高度な設定やコンテナ同士の連携が可能になります。FargateはECSやEKS用のサーバーレス環境であり、AWSユーザーはEC2のようなホストマシンの複雑な設定が必要なくコンテナを運用できます。
GithubとECRを連携させるために、Github Actionsの設定を行います。
初めにAWSのIAMで『AmazonEC2ContainerRegistryPowerUser』のポリシーを設定したユーザーを作成し、アクセスキーを発行します。発行したアクセスキーペアと、あらかじめ作成したECRのリポジトリ名をGithub>Setting>Secrets>Actionsに設定しておきます。
次に作業ディレクトリにて、.github/workflows/main.ymlを以下のような内容で作成します。今回は「tech*」というtagのpushイベントをトリガーにECRにデプロイされるように設定しました。
name: Push and Deploy ECR on: push: tags: - tech* jobs: build-and-push: runs-on: ubuntu-18.04 timeout-minutes: 300 steps: - uses: actions/checkout@v1 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Push to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }} run: | IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g") docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
tagをつけてGithubにpushしてみます。するとGithubでBuildされ、AWSへのデプロイが開始します。
ECRにあるリポジトリのイメージURLをECSでタスク定義として設定し、クラスターで作成したサービス内でタスクを実行してみます。
ECRにデプロイされたイメージを、自動でECSコンテナとして起動させるためには、別でさらにAWS CodePipelineを使用して設定する必要があります。今回はそちらは省かせていただきます。
起動したタスクで発行されたパブリックIPにアクセスすると、無事Github Actionsのイベントでpushされたimageのコンテナを確認することができました。
ここまで技術調査・検証をしてきましたが、クラウドサービスにはたくさんのリソースがある中で、結局、どのリソースを使えば良いのでしょうか?
私の感想としては、今回検証に使用したような非常に簡単なアプリケーション構成で「とりあえずコンテナのデプロイやCI/CDを試してみたい!」というニーズにはCloud Runが適しているなと思いました。 しかし、本来の開発のような、複数のアプリケーションが連携しているシステムで詳細な設定がしたい場合は、FargateやEC2が選択できるECSも選択肢になると感じました。
また、この疑問に対して、GCPの公式の記事では、以下の図でまとめています。
管理工数の少ないコンテナの場合、インフラへの考慮を最小限にとどめることで属人化を防ぎ、エンジニアはアプリケーション開発に集中できる利点により、モダンな開発に浸透している理由がわかります。しかし、サーバーレスで管理の少ない環境であるばあるほど、利用料金は高くなり、カスタマイズの幅が小さくなるといったトレードオフの関係にあります。
開発現場における多種多様なシステム・開発体制に対して、それぞれのケースに適切なリソースを選定するためには、保守運用の人的コストや可用性・移植性等の品質面、また料金面などの複数の観点を考慮し、チームで意見を交え、リソースを選定していく必要があります。
今回は、新卒3ヶ月目の私が、先日のテックフェスで行われたテックトークの内容を現在の開発に活かせないか考え、クラウドサービスのコンテナリソースの調査・検証してみました!
私も今回の調査・検証を経て、クラウドサービスのコンテナリソースやCI/CD・マイクロサービス化についての理解が深まりました。 今後、開発環境をクラウドでの運用に切り替えるタイミングが来れば、積極的に手をあげて環境構築に携わりたいなと思います。
このようにレバレジーズでは年齢や経験に寄らずに、様々な開発の知識・経験が吸収できる環境があります。 若いうちからスキルをつけたい方や、熱意があるチームで毎日学びのあるチーム開発を行いたい方にはこれ以上ない環境だと思います。
ぜひ、みなさんも一緒にサービスを作っていきませんか? レバレジーズに少しでも興味を持っていただけた方は、是非下記リンクを覗いてみてください!
みなさんこんにちは!現在、内定者インターン中の内藤です。 本日は、先日レバレジーズで行われ予想以上に(失礼)大盛況だった「第一回テックフェスのテックバトル」についてお話しします。
この記事を通して、レバレジーズのエンジニア組織の風通しの良さを少しでも感じてもらえると嬉しいです!
テックフェスとは何なのか簡単にお話しします。 テックフェスは、レバレジーズのエンジニア組織メンバー全員を対象としたテクノロジーの祭典です。弊社エンジニアの技術力を向上させ、よりよいサービスを世の中に提供できるようにするために企画されました。
今回、テックフェスは記念すべき第一回を迎えました🎊 その記念すべき第一回目は、以下のようなタイムスケジュールで行われました。
テックバトルでは、3人1組チームに分かれ、コーディングの「正確さ」と「速さ」を競うコーディングバトルを行いました。 コーディングバトルってやっぱりエンジニアならではで楽しいですよね!
では、早速今回のテックバトルで扱った問題を見ていきたいと思います!
今回のテックバトルの問題は、以下の3つの実装を各人が担当し3人で協力して一つのものを作るという内容でした。
※ 今回、企業数と候補者数は同じ()であるとします
※ この安定マッチングとは、企業と候補者、共に現在組んでいるペアよりも希望順位が高いペア(以降では、ブロッキングペアと呼びます)が存在しない状態を指します
上記でも少し触れた通り、この問題の肝はCさんであり、Cさんが如何にこのプログラムを実装するかにチームの命運がかかってます笑
今回、企業(候補者)数 に対して、全組み合わせを力任せで調べるBrute-Forceアルゴリズムを用いると計算量は になります。 が小さい値の時はまだマシですが、 ともなると計算量があまりに膨大になってしまうので、このような時はアルゴリズムの知識が重要になりますね!
今回の問題で活躍するのは、Gale-Shapleyアルゴリズムというもので、このアルゴリズムを用いると計算量は で済ませる事が出来ます。恐ろしいほど強力ですね!😲
今回の問題で重要なのは、如何にしてブロッキングペアの存在しない安定マッチングを実現するかという点です。 Gale-Shapleyアルゴリズムは以下のようにして、安定マッチングを実現します。
では、アルゴリズムを確認したところで、実際にコードを書いてみましょう!
今回は、Aさん, Bさんの実装は置いておいて(ごめんなさい笑)、CさんのGale-Shapleyアルゴリズムに該当する部分のプログラムをpythonで書いてみました。 (pythonにしたのは、自分が日頃業務とは別に使うことが多いからです)
今回使う変数は以下の7つです。
# 初期に与えられている変数 n : 企業(候補者)数 ... 今回、企業数と候補者数は同じ数とします corps : 企業のリスト ... ['A', 'B', 'C', 'D', 'E'] のような形 candidates : 候補者のリスト ... ['a', 'b', 'c', 'd', 'e'] のような形 corp_desire_order : 企業の希望順位リスト ... {'A': ['b', 'e', 'd', 'c', 'a'], 'B': ['c', 'a', 'e', 'd', 'b'],}...のような形 candidate_desire_order : 候補者の希望順位リスト ... {'a': ['B', 'D', 'A', 'E', 'C'], 'b': ['C', 'A', 'D', 'E', 'B'], ...}のような形 # 最終的に出力する変数 corp_to_candidate : 企業と候補者のマッチングリスト ... {'a': 'D', 'b': 'C', ...}のような形 candidate_to_corp : 候補者と企業のマッチングリスト ... {'D': 'a', 'C': 'b', ...}のような形
この7つの変数を基に、プログラムを書くと以下のようになります。
def gale_shapley(corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp): # 1. 候補者とマッチングしてない企業1社が、希望順位が一番高い候補者にマッチングを申し込む for corp in corps: if corp not in corp_to_candidate: free_corp = corp candidate = corp_desire_order[free_corp][0] # 2-B. 1で選ばれた候補者がマッチング中であった場合、現在マッチング中の企業とマッチングを申し込まれた企業の希望順位を比較する if candidate in candidate_to_corp: another_corp = candidate_to_corp[candidate] # 2-B-b. もし、マッチングを申し込まれた企業の方が希望順位が上なら、候補者は現在のマッチングを解消し、マッチングを申し込まれた企業とマッチングをする if candidate_desire_order[candidate].index(free_corp) < candidate_desire_order[candidate].index(another_corp): del corp_to_candidate[another_corp] corp_to_candidate[free_corp] = candidate candidate_to_corp[candidate] = free_corp corp_desire_order[another_corp].pop(0) # 2-B-a. もし、現在マッチング中の企業の方が希望順位が上なら、候補者はマッチングを申し込まれた企業を断る else: corp_desire_order[free_corp].pop(0) # 2-A. 1で選ばれた候補者がマッチングしていなかった場合、候補者は企業からの申し込みを承諾する else: corp_to_candidate[free_corp] = candidate candidate_to_corp[candidate] = free_corp # 3. 1~2を全ての企業と候補者がマッチングするまで繰り返す while len(corp_to_candidate) < n: gale_shapley(corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp)
書いてみると意外とシンプルですよね。 皆さんも是非他の言語でチャレンジしてみてください!
今回、テックバトルを通して、「新しいアルゴリズムを知れたから良かった!」というよりは、エンジニア同士の交流が何よりも貴重で楽しかったです笑
レバレジーズのエンジニア組織は、技術力だけではなく、今回のイベントのようなエンジニア同士の交流も大事にする組織です。日頃からこのようなイベントを行うことで部署間の壁がなく、気軽に悩みを相談出来る環境が常に整っており、とても働きやすいと日々感じています!
皆さんもこんなレバレジーズで一緒に働いてみませんか? レバレジーズに少しでも興味を持っていただけた方は、是非下記リンクを覗いてみてください!
こんにちは。21年新卒の益子です。現在私は、看護業界に特化した人材サービス「看護のお仕事」において、求人サイトのUX改善やそれに伴うシステム開発、ディレクションに携わっています。
看護のお仕事では、サイト改善において指標となる定量データだけでなく、「看護師さんにとっての使いやすさ」などの定性データも重視して改善を進めています。
このようなサイト改善の取り組みに対しては、事業部としても知見が浅く、常に手探りの状況でしたが、私はユーザビリティテスト・デプスインタビューなどのUXリサーチやその結果を元にした改善施策の立案・実装・効果検証までを一貫して担当し、1年間でCVR(※1)を13%改善しました。
(※1). Webサイトへのアクセス数、またはアクセスしたユーザー数のうち、コンバージョンに至った割合を示す指標。転職エージェントサービスが運営する求人サイトの場合、求人応募やサービス登録に至ったユーザーの割合を示す。
この記事ではエンジニアとして新卒入社してからUX改善に取り組み始めたきっかけ、取り組みの中でぶつかった壁、どのように問題を解決しCVR改善という成果に結び付けたかについてまとめたいと思います。
私が求人サイトのUX改善に取り組み始めたきっかけは、ユーザビリティテストに同席した際に希望する求人を探せずに戸惑っているユーザーの様子を目の当たりにしたことでした。
ユーザビリティテストでは被験者に対し「東京都渋谷区で、希望する条件に一致する求人を検索して表示して下さい」といったタスクをお願いし、モデレーターの目の前でタスクを実行してもらいます。被験者の実際の操作を通して、スムーズに求人を検索できるか、希望条件に一致する求人を絞り込めるかなど、ターゲットユーザーにとってのプロダクトやサイトの使いやすさを評価していきます。
初めて同席したユーザビリティテストで見たものは想像と大きく異なり、「希望条件に対して、サイト上のどの絞り込み項目が適切なのか迷っている」「絞り込み条件を増やしすぎてしまい希望する求人を見つけることができない」というユーザーの様子でした。
私はこの体験を通し「どれだけ努力して実装してもユーザーにとって使いにくいようでは無意味。看護師さんにとって使いやすい求人サイトを作るために開発業務以外もできるようになりたい。」と思うようになりました。
ユーザーにとって使いやすいプロダクトを作るためには、改善施策の全ての議論をユーザー起点で行う必要があると考えました。さらに具体的に言うと、サイト改善に関わるメンバー全員が、架空のユーザー像を元に議論し合うのではなく、実際にユーザーに触れて得た一次情報を元に議論できる状態になる必要があると考えました。
ユーザーの一次情報から議論を行えるチームになるため、以下の3点を提案し、チームで実行しました。
まず看護師転職の課題について解像度を上げるためにデプスインタビューを行いました。
デプスインタビューとは、特定テーマについて対象者と1対1でインタビューを行う定性調査手法です。対象者の表面的な行動・思考だけではなく、行動・思考の背景にある事情や、対象者ですら言語化できていない課題を明らかにすることができます。
デプスインタビューでは、調査したいテーマに合わせた対象者の選定が調査の成果を左右します。私たちは調査結果を求人サイトの改善施策に落とし込みたかったため、「転職検討中であるが、転職エージェントサービスの利用経験がない人」を対象とし、5名に対し調査を実施しました。
転職エージェントサービスの利用経験がある人は、求人サイトの使いやすさよりも、過去の利用体験を検討材料としてサービスを判断する可能性があります。そのため、転職エージェントサービスの利用経験がない人にインタビューを実施することで、転職を通して得たいものに対し、どのように求人サイトを利用するのか・転職サービスに何を期待するのかを明らかにしようと試みました。
デプスインタビューを通して得られた示唆は大きく2点でした。1点目は転職を通して得たいものやその理由、2点目は求人サイトを「単純な求人検索ツールとして使用するのではなく、その求人サイトを運営する転職エージェントサービスを比較するために使用している」という実態です。
その示唆に対し、オープンタスクでユーザビリティテストを行い現状のサイトを再評価しました。単純にタスクをお願いして実行してもらうのではなく、「このページではどのような情報が得られましたか?」「次はどのように操作したいと思いますか」「サイトを離れた後は何をしたいと思いますか?」というオープンタスクでサイトの評価を行いました。
これにより「なぜページ内のコンテンツが読み飛ばされているのか」「なぜ特定のボタンが頻繁にクリックされるのか」など、なぜユーザーはこのように操作するのかというところまで定性データを収集することができました。
デプスインタビューやユーザビリティテストでユーザーに対する解像度が高まり、チーム内で改善施策の議論が活発になりました。
その議論を”ユーザー起点”という観点で質のよいものにするため、「タスク管理ツールに改善施策案を起票する際、ユーザーを起点に仮説を言語化する」というルールを作り、徹底しました。
具体例を挙げると「求人応募ページの応募ボタン文言が〇〇であることにより、ユーザーは違和感を覚えて求人応募を完了することができないのではないか」という起票内容です。
このルールを履行することで、改善施策においてユーザーを軸に仮説を立てることが仕組み化され、常にユーザーの一次情報を材料とした議論を行うことをメンバー全員に定着させることができました。
前述の3点の取り組みにより、サイト改善における議論の質が高まりました。しかし、いくら議論を重ねても、すべての施策で改善傾向が出るとは限りません。
実際に、チームメンバー全員で考え抜いた施策をリリースした際、CVRの悪化傾向が出てしまったことがありました。
サイト全体のコンバージョンへのインパクトが大きい求人詳細ページのUIを大幅に変える施策を打ち出した時のことです。プロトタイピングでのユーザビリティテストを何度も実施しながら施策内容のブラッシュアップを重ねていたものの、実際に施策をリリースしてABテストを実施したところ、CVRが大きく悪化する結果が出てしまいました。
インタビューやユーザビリティテストを重ねながらメンバー全員で考え抜いて打ち出した施策だったため、各数値指標の悪化を目の当たりにした時には、次の打ち手を見失う状況に陥りました。
この状況に陥った根本的な原因は、定性データから打ち出された施策効果を検証する仕組みが整っていなかったことでした。
Aパターン・BパターンそれぞれのCVRの結果や各ページの遷移率などの定量的な施策効果の検証に関しては以前から仕組み化されていましたが、「施策内容によってユーザーの操作はどのように変化したか」「なぜ変化したか」といった定性的な施策効果を検証する仕組みはなく、1から構築する必要がありました。
私はこの状況を改善するため、リリースした施策を定量・定性両方のデータで検証することを提案し、チームメンバーで実行に移しました。UI上、施策の要点となるすべての箇所にクリックログを実装してクリック率やその後のコンバージョンを計測しながら、ユーザビリティテストも並行して実施し、操作の背景にあるユーザーの心理を探るという検証方法です。
これにより施策の仮説が途切れることがなくなり、1つの仮説から後続施策・新しい仮説が生まれる状態を作ることができました。
前述した求人詳細ページのUIを大幅に変える施策も、定量・定性両方のデータで検証することで、求人応募ページへ遷移するボタンが致命的な悪化要因となっていたことを発見することができました。その後の追加施策でボタン部分を改修し、98%以上の有意差でマイクロコンバージョンの完了率を29%改善することができ、CVRを向上させることができました。
これらの取り組みを継続した結果、ユーザーを起点とした施策立案・施策のリリース・定量と定性の両方を駆使した効果検証・追加のブラッシュアップ施策の実行まで、UX改善のPDCAサイクルを回すことができるようになりました。
その変化は数値成果に表れ、1年間で最重要指標であるCVRを13%改善することができました。
また、UX改善のPDCAサイクルは途切れず、施策の確度も向上しています。施策立案においてはこれまでに実施していなかった新しい定性調査手法やヒューリスティック評価を取り入れ、施策実行においては開発メンバーが着手優先度を柔軟に調整することでABテストの実行数が増加しています。
サイト改善に関わるメンバー全員が、前年度以上の成果を出そうとモチベーション高く取り組んでいます。
昨年度一年間の取り組みは、自分自身の今後にも大きな変化を与えたと思っています。
開発職として入社したものの、「ユーザーが迷いながら操作している」という現場に遭遇したことで、それまで実務で取り組んだことがなかった「UX」という分野に取り組むきっかけができました。
また開発業務の枠を超えて、リサーチ・施策立案・施策の実装とリリース・効果検証の全てのプロセスで提案と実行を行う中で、レバレジーズが顧客体験の改善という観点で優れているところ・未熟なところの両方が見えてきました。
上記の経験を通し「エンジニアでありながらマーケティング分野にも職域を広げたい」という思いが「レバレジーズの顧客体験の改善を自分が牽引できるようになりたい」という思いに変わりました。それを実現するために業務内容も少しずつ変わり、昨年度はマーケターが持っていた「UX改善によるCVRの向上」というミッションが、今年度からは自分のミッションとして与えられました。
レバレジーズには社員一人ひとりが事業や担当業務の課題を見つけ、新しい取り組みを提案する・責任をもって実行するという行動を評価する文化があります。そこには年次はもちろん、職種の枠も関係なく、その文化があったからこそ自分の目標に近づくための働き方ができていると感じています。
レバレジーズには事業やプロダクトの成長に、開発の職域を超えて取り組んでいるエンジニアが多く在籍しています。開発職として技術を磨きつつ、事業やプロダクトの成長に幅広く関わっていきたいという方は、カジュアル面談などでレバレジーズのエンジニアと話してみませんか?きっと面白い発見があると思います。
ご興味がある方は是非こちらからエントリーをお願いします!
この記事では、未経験から短期間でリーダーになるためのマインドセットをご紹介したいと思います。
僕のように未経験でエンジニアになる方、リーダー経験を若いうちに積みたい方には、ぜひ読んでいただきたいなと思っております!
初めまして、エンジニアの五十嵐です。
僕は、2020年10月からレバレジーズで内定者インターンを開始し、2021年4月に入社してから現在に至るまでHRテック事業部という新規事業の部署に所属し、SaaSの開発を行っております。
学生時代は、小、中、高、大学とサッカーに青春を費やしてきたスポーツ少年でした。
大学では、情報系や理系の学部に所属していた訳ではなく、文系の商学部に所属していたので、Webエンジニアとは無縁の学生時代を過ごしておりました。
入社前は、自分で家計簿アプリやTwitterの模倣アプリ、シフト管理アプリなどを作ったり、Paizaなどでアルゴリズムを考える問題を解いたりしていた程度でした。
きっかけは就職活動でした。
様々な業界の仕事や業種、企業を知りたかったので、金融系や日経大手の企業など幅広く受けていました。
多くの企業や業界を見ている中で、ITのWeb業界に興味をもち、様々な人と会って話を聞いていくうちにエンジニアという道もあることを知りました。
そして、エンジニアの働き方や仕事内容などに興味をもち、実際に自分でプログラミングを学び始めたのがきっかけでした。
僕は、新しい物事や常に変化する状況に対してモチベーションを高く持って行動するタイプです。レバレジーズ選考時にその旨を会社側に伝えていました。
内定後は、すぐにでも実務の経験を積みたいと考え、内定者インターンの希望を会社に出していました。
内定者インターンをするに当たってどの部署で働きたいかを人事と面談している時に、新規事業が動き出している話を聞き、やってみないか?という提案をもらいました。
僕は、裁量権があり、幅広い経験ができる会社だと思ったからこそレバレジーズの内定を承諾しましたが、まさかこんなにすぐにチャンスがくるとは思わず驚きました。
こんなチャンスはもしかしたら2度と来ないかもしれないと思い、新規事業でインターンをしたい!と伝え、参画することになりました。
業務として経験したことは以下になります
使用技術は以下になります
FE
BE
Infra
external-service
0からの開発かつ、初期メンバーは5人だったので、インターン生であっても上流工程から下流工程まで経験させてもらいました。 バックエンドを主に担当し、GraphQLでAPIの開発をマイクロサービスアーキテクチャで行っていました。
設計や開発を行う中で、未経験の僕が意識したことが以下の3つになります。
1つずつ順を追って説明していきます。
当初はGraphQLを知らず、SQLの仲間かなとか思っていましたし、DB設計の知識も全くありませんでした。 そんな僕ではありますが、個人的に決めていたことがありました。
それが、「わからないことを、すぐに質問をしないこと」です。
僕の先輩が営業からエンジニアにジョブチェンジした方で、「とりあえずチャレンジしよ!」というポジティブでレバレジーズっぽい方でした。
それに習って、僕もわからないことがあった場合でも、上司やメンバー、業務委託の方の設計書やコードをみて学んで、わからないことは調べて、自分なりのアウトプットを出していました。
アウトプットを出すことで自分の考えも整理でき、レビューで指摘された場合でも意見を伝えることができていました。
このやり方は、インターン開始当初はタスク消化の遅延を出してましたが、その時に吸収されたものが数ヶ月後になって活きてくるようになり、徐々にスピード感を持って業務を行えるようになっていきました。
なので、このブログを読んでくださっている新人エンジニアの方はわからない技術や物事は自分で学んでどんな形であってもアウトプットを出すことをオススメします。
先輩と1on1を行っている時、何か個人的な目標を作ると良いとアドバイスをもらい、1年間の目標として「困った時は五十嵐」という立場になることを設定していました。
そして、その状態はどんな状態を指すのか考えた時に、誰よりもサービス全体の理解と使用技術の理解をしている状態だと考えました。
そのために、僕は任されているタスクを完璧にこなすことはもちろんですが、業務以外の空いた時間や休日などを利用して以下のことを自主的にキャッチアップしていました。
これらのことは、「困った時は五十嵐」という立場になるためにやっていたことではありますが、新しい物事を学ぶことが楽しくて、つい時間を忘れてしまうこともしばしばありました。
特にアーキテクチャ周りの勉強をする前と後ではサービスの見え方が変わりましたし、改善点も見えてきたのでスキルアップしたなという実感ができました。
僕がレバレジーズを選んだ理由の一つが、早い段階でリーダー業務を経験したかったことが挙げられます。
そのため、与えられた自分のタスクだけこなすのではなく、主体的に行動し関係する影響範囲を広げることで、自らのスキル向上とリーダーからの信頼獲得を意識していました。そしてその後、少しずつリーダー業務を任されるようになっていきました。
具体的には以下の4点です
この中でも、「リリーススコープを考慮した意思決定」の部分は早めに吸収してよかった点だったと思っています。
なぜなら、メンバーの僕が見えている部分とリーダーから見えている部分が違うことがわかり、よりサービス全体が見れるようになったからです。
リーダーは、より高い位置から俯瞰してサービス全体をみており、広い範囲の仕様を把握をして、様々な情報や観点から意思決定をしていました。
なので、僕もリーダーに対して提案を行う時は、担当機能の仕様把握だけでなく、サービス全体の仕様とその影響範囲の把握をしてから提案をするようにしていきました。
より俯瞰してサービス全体を考えた提案をすることでリーダーから信頼を得て、リリーススコープ内の仕様の選定や機能実装の際の意思決定を少しずつ任されるようになっていきました。
設計や実装時のわからない時にインプットのために読んでいた本がいくつかあるので紹介します。
単にコードを書くだけではなく、わかりやすく、そしてメンテナンス性の高いコードを書くために、気を付けたいことなどが書かれています。
実践的なテクニックや心構えが中心で、例えば「どんな名前付けをした方が良いか」ということ等が紹介されています。
わかりやすいソースコード、設計についてオブジェクト指向設計の特徴を生かしながら具体的に説明してくれる本となっています。
実務で役立つような内容となっており、設計について完全に初心者でも、具体的な説明で設計に何が必要なのかを理解できます。
ソフトウェア開発の原則のSOLID原則などの設計についてやデザインパターンとケーススタディのコードが書いてあります。
新人エンジニアには少々難しい内容ではありますが、知っているのとそうでないのではスキルアップに大きく差が出ると思うのでおすすめです。
この本は、いわゆる設計手法としての「クリーンアーキテクチャ」についての話かと思いがちですが、
内容はアーキテクチャとは何か、どういった目的意識があるのかというアーキテクチャ全体の概念で、非常にためになる話が多くいくつも学びがありました。
これらのことを意識して、ひたむきに業務やサービスに向き合っていたことで、入社2年目となる現在はリーダーを任せてもらっています。
リーダーになってからは、開発業務以外に、チームのタスクの作成や優先度付け、上層部へのエスカレーション、メンバーのピープルマネジメント等も行っています。
マネジメントの難しさに悩むことも多々ありますが、関連書籍で学んだり、上長などに相談するなどして自分の業務の幅を広げています。
僕の経験でもわかるように、レバレジーズは経験や年齢で判断せずにチャンスを与えてくれる環境です。
若いうちから様々なチャレンジをしたい方にはこれ以上ない環境だと思います。
また、未経験で不安があった僕ですが、今では毎日学びの連続で、責任感はありつつも非常に楽しく働くことができています。
ぜひ、みなさんも一緒にサービスを作っていきませんか?
レバレジーズにご興味ある方は、こちらからエントリーお願いします!
こんにちは。2021年新卒の田中です。現在私は、フリーター・既卒・第二新卒など、20代を対象とした就職・転職支援サービス「ハタラクティブ」のシステム開発やユーザー体験の改善に携わっています。
ハタラクティブでは、Laravel + jQueryだったフロントエンド環境をReact/TypeScript + Next.jsにリプレイスしました。その際、コンポーネントシステムとしてAtomic Designを採用し、入社1年目ながら選定から設計、導入プロジェクトの管理、運用ルールの構築を任せていただきました。
この記事では、ハタラクティブでどのような課題を抱えていてAtomic Designを採用したのか、導入を進めるにあたってどのような問題が発生し、どう解決したのか、メリットを含めてご紹介します。
ハタラクティブではフロントエンドをReact/Next.jsで開発していますが、当初はコンポーネント管理に関するルールが決まっていない状態でした。そのため、同じ記述が複数箇所にあったり、同じUIパーツが共通化されていなかったり、無駄な記述や重複が多く、開発コストが余計に増えたり保守性が下がる問題や、修正作業が属人化する問題を抱えていました。
当時は、スタイルガイドが機能していなかったため、チーム内でデザインについての共通認識を取れず、作成したUIパーツを使い回す体制も整っていませんでした。その結果、同じUIでもページによってデザインが異なっている状態が発生していました。
以上のことから、ハタラクティブオウンドメディアチームでは開発コストを小さく抑え、デザインが統一された状態を作る課題があったため、導入事例やドキュメントが多いAtomic Designを採用し、コンポーネントシステムを構築することを提案しました。
当時は本格的にAtomic Designを使った開発経験があるメンバーがいませんでした。どのように導入すればいいかのイメージがすぐに湧かず、チーム内でコンポーネント化の共通認識が持てない状況でした。
そのため、私が主導して他サービスのAtomic Design導入事例の記事や社内の他チームへのヒアリングを通じて、Atomic Designに関する知見を集めました。自分で試しにテンプレートファイルのAtomic Design化を実装し、そこで得られた知見に基づき、チームで導入の方針や定義、懸念点の解消方法を明確にするための話し合いの場を複数回設けました。
コンポーネント化を進めることで今までのStyleの記述方法を大きく変える必要があるなど、ハタラクティブへのAtomic Design導入コストの大きさの懸念もありました。
導入コストに関しては、既存コードの整理や将来的な開発コストの削減を目指す方針で認識を合わせ、通常施策とは別のリファクタリングタスクを通じて長期的に進めていきました。Styleに関してはNext.jsでサポートされているCSS Modulesを採用しました。CSS ModulesはCSSをmoduleに分け、JSファイルでimportすることでスタイルを適用する手法です。コンポーネントごとにmoduleファイルを分けることでスタイルをコンポーネントに閉じることができ、class名の干渉を考慮する必要なく記述することができます。
/* Example.module.scss */ .title { font-size: 1.6rem; color: #222; } .box { padding: 8px 16px; background: #eee; }
// Example.tsx import styles from "./Example.module.scss" const Example: React.FC = () => { return ( <> <h2 className={styles.title}>タイトル</h2> <div className={styles.box}> {/* 中にコンテンツ */} </div> </> ) } export default Sample
CSS Modulesの導入から記述方法の共有を主導し、デザイナーがコンポーネント単位の書き換えを徐々に進めていくやり方を取りました。
Atomic Designの基本を理解し、ハタラクティブに導入する上での解釈をすり合わせ、コンポーネント分けルールを作成しました。ルールの一部を紹介します。
例えば、MoleculesはAtomsやOrganismsと異なり定義が難しかったため、再利用性を判断軸に置き、分類に迷った場合は一旦Organismsにする運用方針に決めました。 データの取得や保持に関しては、統一性と再利用性を高める目的で、API通信に関する機能や各ページで異なるデータはPagesのレイヤーに、固有のデータはOrganismsに持つようにしました。
方針とルールが固まったら、どういったスケジュールで進めるのかを決めました。実際のプロジェクトは準備編、試験導入編、実装編の3つのフェーズに分けて進めました。
準備編では「何をやるのか」「どこまでやるのか」が決まっている状態をゴールに据え、既存デザインパターンの洗い出しとコンポーネント設計を行いました。デザイナーと一緒に既存パーツのバリエーションを精査しページ全体のトンマナやデザインを統一しながら、コンポーネント分けルールに沿って分類と設計を進めました。
試験導入編は「チーム全員がやることを理解し、導入方針に納得している状態」をゴールとし、本格的な実装の前に工数を取って全員がAtomic Designを試す週を設けました。複数の視点から見えてくる問題や疑問を事前に解消し、スムーズに実装を進めることを目的としていました。
実装編は「通常施策への影響を小さく抑えながら導入を完了する」ことがゴールで、Atoms→Molecules→Organismsと小さいパーツから順に実装していきました。 スタイルガイドが機能していない問題に対しては、storybookの導入を決めました。 storybook.js.org storybookは、UIコンポーネントの管理やアドオンを使ったテストなどができるツールで、コンポーネント化と並行してスタイルガイド作成を同時に進めることにしました。
こうして約半年にわたるスケジュールを作成し、導入プロジェクトを進めていきました。
準備を重ねて実装編に入りましたが、いくつか問題があがってきました。
Atoms、 Molecules、 Organismsだけでの管理は、ディレクトリを増やさずに済むのでシンプルさを維持できたり、世の中にリファレンスが多いメリットがあります。しかし、エントリーフォームなど複雑なUIに対しては「Atomsは最小単位で他のコンポーネントを含まない、MoleculesはAtomsを1つ以上含む」などのルールが適用できず、例外が発生してしまうのが避けられない状況でした。
それに対し、ハタラクティブ独自のディレクトリルールを追加するのは、慎重に考える必要がある一方で、ハタラクティブのデザインに合わせて管理しやすい形を実現できるメリットがあります。
そのため、既存のAtomic Designの形を大きく変えないように注意しながら独自のディレクトリルールを作成しました。具体的には、Layouts, Transitions, PageContentsディレクトリなどを追加し、それぞれレイアウトを共通化するコンポーネント、カルーセルやアコーディオンなどのインタラクションを共通化するコンポーネント、ページ全体のStyleやStateに関するコンポーネントを管理するようにしました。
導入プロジェクトが始まってしばらく経ったタイミングで作業の属人化が問題となり、2つの問題に分割して対処しました。
1つ目は基準が明確化されていないことです。準備編で作成したルールを見直し、Atomic Designやハタラクティブのデザインパターンに関する知識が少ない人でも理解できるように、コンポーネントの各ディレクトリの役割や実際に分ける際のフローをドキュメント化し共有しました。
2つ目は、慣れの問題です。プロジェクトの進捗を優先していたことで特定の人にAtomic Designタスクが集まっている状態になっていました。限られた人しか出来なければ導入後の維持が困難になります。そのため、なるべく全員が実装タスクを担当するようにし、私がレビューに入るようにしました。
導入の過程でデザインを精査したことでデザインの統一が実現し、共通コンポーネントを使うことで微妙に違うデザインが生まれることが防げるようになりました。
storybookを使った管理をすることでデザイナー・エンジニア間のUIパーツの認識合わせや、どんなパーツがあるかの把握が容易になりました。
コンポーネントが共通化されたことにより、変更を加える際の対応箇所が1箇所、ないしは少なくなって開発速度が向上しました。
それまでは修正漏れを避けるために担当者が固定されて作業が属人化していましたが、ファイルが統一されたことで誰でも修正を担当できるようになりました。
Atomic Designの導入がほとんど終わってきた段階で、サービスのブランドコンセプト見直しに伴う大規模なデザイン変更がありました。その際、コンポーネント化によって新デザインの反映がスムーズになったり、ルールが作成されていたことで新規UI追加の際も深く悩むことなく実装ができました。
新規コンテンツとして求人検索ページの実装タスクでも力を発揮しました。求人検索ページはボタンなどの既存パーツを使いつつ、検索パネルなど新規のUIも多いコンテンツでしたが、storybookやコンポーネントルールにより作業効率を高めることができたことで2週間という短い時間での実装を可能にしました。
Atomic Designの採用によって、開発コストの削減とデザインの統一という課題を解決し、オウンドメディアとして今後の成長を加速させていく基盤を作ることができました。しかし、Atomic Designを導入さえすれば解決できるというものではありません。職種を超えて課題について議論し、常により良い方法がないかを模索できるチームがあったからこそ実現できたと思っています。
配属から2ヶ月足らず、Reactに対する経験も浅い状態で、今回のAtomic Design採用提案から導入まで担当させていただきました。このように、レバレジーズには年次や経験を問わず、主体的に取り組める環境が整っています。
レバレジーズは「主体的に仕事に取り組みたい」「エンジニアの技術を活かしてサービスの課題を解決したい」と思っている方が活躍できる環境です。
ご興味を持たれた方は下記リンクからご応募ください。
こんにちは、レバレジーズ フリーランスHub開発チームです。 現在、弊社が運営するフリーランスHub というサービスでは検索エンジンとしてElasticsearchを採用しています。 この記事では、エンジニアがサービスの成長速度と工数を考慮した上で、Elasticsearchを採用するに至った背景や理由、そして、MySQLからElasticsearchへ移行した結果どれくらいパフォーマンスが改善したか、などを紹介していきたいと思います。
フリーランスHub は全国のフリーランスエージェントの保有案件から、10万件以上の案件をまとめて掲載しているエンジニア・クリエイター向けのフリーランス求人・案件メディアです。
2021年4月にリリースした新規事業で、サイト内でフリーランスエージェントの案件を集約して掲載しており、フリーランスを検討される方が案件に一括で閲覧・応募することができます。
また、サイト内で開発言語や職種別に案件の検索ができることに加え、エージェントの詳細ページから各エージェントの特徴やサービスを受けるメリットなどを比較し自分にあったフリーランス案件・フリーランスエージェントを探すことが可能です。
検索パネルは開発スキルや職種、業界、こだわり条件(週3など)や、都道府県、路線、駅での検索機能があります。
事業をスタートする際は様々な軸で数年の見通しを立てます、今回のテーマに影響するものはこの辺です。
フリーランスHubは機能数が少ないので小さくスタートし、短期間で拡張と機能追加をしてサービスを成長させていく方針にしました。 その他見通しの説明は記事の最後で補足しておきます。
AWSを利用している他プロジェクトの導入実績をベースに選定、Elasticsearchでの全文検索は幾つかの事業チームでの運用実績があります。 RDSは2つあり、メインはAurora(MySQL)、データ収集と分析を低スペックのMySQLにしています。 集まってくれた開発メンバーの中にElasticsearchを熟知しているメンバーがおらず、安定性と開発速度の両面で懸念が出てきました。
検討
決断
まずはRDSだけでシステムを構築し、切替前にElasticsearchにデータを同期した後、検索機能を切り替える方法を選択しました。
リリース前 (3万件〜)
機能追加・拡大 (9〜12万件)
処理説明
サイトからの検索処理
検索データを上手に作成しておく事が必要となる代わりに、検索SQLでテーブルをジョインする必要が無くシンプルになります。 Elasticsearch利用時も検索テーブルだけのシンプルなクエリに出来ます。
インフラ構築
Elasticsearchは最終的なスペックでビルドし利用するまでスペックを落として運用していました。 AWSはCDKで構築したのでサイズとノード数を変更し実行するだけで切り替わる構成です。
Elasticsearchに限らず初期は不要なケースも最初から作成しておきました。例えば、MQはRDSでスタートし後でSQSに切り替えました。 インフラ変更は影響範囲が広く、後で追加を行う時の苦労が多いため、構成は最初から作っておき利用時に起動する等により、サービスの安定性と成長スピードを損なわないメリットがあります。 無駄なコストが発生してしまうので、サービス成長速度を上げ無駄となる期間を短くします。
早速結果から、クエリキャッシュOFFで実行しています。 MySQL long_query_time は 0.25s を閾値にして運用しています。
案件数が5万件ほどの時期にソート順の要件が増えてきました。
指定した検索条件で関連順の場合はMySQL FULLTEXTインデックスでも高速ですが、独自にソートを追加したSQLは速度低下が見られました。 ダミーデータで作成した32万件のテーブルにクエリ実行した結果、ヒットした件数が増えるほどソート処理に大きく時間がかかる事がわかりました。 契約数・掲載数も順調に伸びているため、早めにElasticsearchに切り替えることにしました。
1週間ほどエンジニア1名をElasticsearchに貼り付け32万件のダミーデータでクエリを発行し大きく改善する事を確認、他の追加開発を止める事もなく、その後3週間ほどで切り替えが完了。 トークナイザーやデプロイの最適化など、経験と専門性が必要な領域は切り替え後に作り込みました。
テーブル、インデックスは item_search です。
CREATE文は補足に記載しておきます。
初期
SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE MATCH(search_string) AGAINST( -- キーワードに「東京」が含まれる案件の中で、「Java」もしくは「PHP」の案件の取得 ' +( ""東京"") +( ""Java PHP"")' IN BOOLEAN MODE );
+優先+募集終了
( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE -- `item_id` = 25000,30000,35000,40000,45000は上位表示する優先データ `item_id` IN(25000, 30000, 35000, 40000, 45000) ORDER BY `updated_at` DESC ) UNION ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 1 AND MATCH(search_string) AGAINST( ' +( "東京") +( "Java PHP")' IN BOOLEAN MODE ) ) UNION ALL ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 2 AND MATCH(search_string) AGAINST( ' +( "東京") +( "Java PHP")' IN BOOLEAN MODE ) );
上位表示する優先データは別のクエリで先に求めています。 上2つのSQLをUNIONしているのは上位表示したデータの重複を防ぐためです。ヒット件数が多い検索条件であるほどソート処理に時間がかかります。なおUNION ALLにしてもヒット件数が多い限りほぼ改善しません。 また、WHERE句でMATCH()を使用すると自動的に関連度順でソートされますが、SELECTステートメントでもMATCH()を使用する事でORDER BY句で関連度順ソートを指定する事ができ、statusと併せてソートする事も可能です。ただ、前者に比べ後者の方がソートのオーバーヘッドがかかるため後者は採用しませんでした。
+優先+募集終了
GET /item_search/_search { "track_total_hits": true, -- 表示する案件は1ページあたり30件のためsizeを30で指定 "size": 30, "query": { "bool": { "must": [ { "match": { "search_string": { "query": "Java PHP", "operator": "or" } } }, { "match": { "search_string": { "query": "東京", "operator": "and" } } } ], "should": [ { "terms": { "item_id": [ 25000 ], "boost": 10000 } }, { "terms": { "item_id": [ 30000 ], "boost": 8000 } }, -- <中略> -- { "terms": { "item_id": [ 45000 ], "boost": 2000 } } ] } }, "sort" : [ { "status":{ "order" : "asc" } }, { "_score":{ "order" : "desc" } } ] }
上位表示する優先データはスコアをブーストする方法で実現しています。
LaravelのScoutを用いて、Auroraの検索テーブルを追加・更新した場合Elasticsearchに自動的に同期される作りにしました。 そのため、モデルをElasticsearchと自動的に同期させるモデルオブザーバを登録するために、検索可能にしたいモデルにLaravel\Scout\Searchableトレイトを追加しました。
use Laravel\Scout\Searchable; class Post extends Model { use Searchable; }
以下のクラスを作成し、Elasticsearchで検索できるように設定しました。
また、SQLを呼び出す側だったRepository層では、SQLからElasticsearchのクエリに書き換えました。
1文字は単語として認識しないため、検索データ・検索クエリ共に変換して処理しています。 認識しない記号も同じように変換しています。 駅名は検索データに「渋谷」「渋谷駅」の2つを登録し、検索パネルの場合「渋谷駅」に変換し「渋谷区」の別の駅にヒットしないようにしています。 これらの文字列はmecab、kuromojiに登録しておきます。
デフォルトは最小4文字ですが「DB」「東京」などあるため2で設定します。
ft_min_word_len=2 innodb_ft_min_token_size=2
変更する場合はDB再起動が必要なので必ず先にやっておきましょう。
MySQL FULLTEXT インデックスはWITH PARSER を指定してINDEXを作ります。
スキル(PHP・Linuxなど)での検索の重要性が高いためngram無しでINDEXを作成しました。
ALTER TABLE `item_searches` ADD FULLTEXT KEY `idx` (`search_string`);
例えば「...業務内容はブリッジSEとして...」を解析しても「ブリッジSE」ではヒットしない挙動となり、 Elasticsearch移行前の大きな課題でした。
modeをnomalに設定することで、辞書登録した文字列を1単語として単語認識させることができます。
"tokenizer" : { "kuromoji" : { "mode" : "normal", "type" : "kuromoji_tokenizer" } }
モードがデフォルトのsearchだと、例えば javascriptを形態素解析した場合、javascriptの他に java script としても分解されるため、検索結果にjavaの案件が含まれる事になります。 今回の事例では辞書登録した単語を1単語として単語認識させることで検索結果の精度を上げています。
char_filterで検索文字列の大文字を小文字に統一する設定も行なっています。
"char_filter": { "lc_normalizer": { "type": "mapping", "mappings": [ "A => a", "B => b", "C => c", --- <中略> --- "Z => z" ] } },
Character Filterを使い入力文字列を小文字に正規化することで、大文字・小文字に関係なく(※)辞書登録されている文字列に一致させることができます。
※ ユーザ辞書を作成する際にアルファベットは小文字限定で作成している、という前提があります。
artisanコマンドを作成し、以下を実行しています。
簡単ではありましたがElasticsearchを採用するに至った背景や理由、MySQLからElasticsearchへ移行した結果どれくらいパフォーマンスが改善したかを紹介しました。
今回の事例のように、レバレジーズでは事業課題を解決するための技術選定やプロダクト改善をエンジニアが主導して行うことができます。 このような主体的に提案ができる環境で是非一緒に働きませんか?
レバレジーズ フリーランスHub開発チームでは一緒にサービスを作ってくれる仲間を募集中です! ご興味を持たれた方は、下記リンクから是非ご応募ください。
MySQL FULLTEXT インデックス
CREATE TABLE `item_searches` ( `id` int(11) NOT NULL, `item_id` int(11) NOT NULL, `base_value` int(11) DEFAULT NULL, `status` tinyint(4) NOT NULL, `search_string` text NOT NULL, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ALTER TABLE `item_searches` ADD PRIMARY KEY (`id`), ADD KEY `item_searches_item_id_index` (`item_id`) USING BTREE, ADD FULLTEXT KEY `idx` (`search_string`), MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
Elasticsearch
PUT /item_search?pretty { "aliases" : { "item_search_replace" : { } }, "settings" : { "index" : { "max_result_window" : "500000", "analysis":{ "char_filter": { -- 2022/03/10追記 "lc_normalizer": { "type": "mapping", "mappings": [ "A => a", "B => b", "C => c", --- <中略> --- "Z => z" ] } }, "filter" : { "greek_lowercase_filter" : { "type" : "lowercase", "language" : "greek" } }, "tokenizer" : { "kuromoji" : { "mode" : "normal", "type" : "kuromoji_tokenizer", "user_dictionary_rules" : [ "Java,Java,Java,スキル", "PHP,PHP,PHP,スキル", -- <中略> -- "渋谷駅,渋谷駅,渋谷駅,駅名", "東京駅,東京駅,東京駅,駅名" ] } }, "analyzer" : { "analyzer" : { "type" : "custom", "char_filter" : "lc_normalizer", -- 2022/03/10 追記 "tokenizer" : "kuromoji", "filter" : ["kuromoji_baseform", "greek_lowercase_filter", "cjk_width"] } } } } }, "mappings" : { "properties" : { "id" : { "type" : "integer" }, "item_id" : { "type" : "integer" }, "search_string" : { "type" : "text", "analyzer": "analyzer", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "base_value" : { "type" : "integer" }, "status" : { "type" : "integer" }, "created_at" : { "type" : "date" }, "updated_at" : { "type" : "date" } } } }
こんにちは。レバレジーズ株式会社エンジニアの加藤です。 今回は、私が所属しているHRテック事業部における新規SaaSの開発についてご紹介したいと思います。
私たちの事業部が扱う領域はHRTechといい、主に会社の人事業務に対するサービスを展開しています。「人事」と聞くと採用活動をイメージされることも多いですが、ここでは「従業員が入社し、退職するまでに関わる業務の全般」を指します。そのため、採用管理や勤怠管理、タレントマネジメント、給与管理などをはじめとした、さまざまなSaaSが存在しています。勤怠管理サービスなどは使ったことのある方も多いのでイメージしやすいのではないでしょうか。
人事業務は人材採用、人材育成、離職対策、人事評価、労務管理など多岐に渡り、従業員が入社から退職するまでなくてはならない業務です。適切に人事業務が提供されない会社では従業員は力を発揮できませんし、当然ながら定着もしません。つまり、人事業務が最適化されているかどうかは「会社の発展や成長」を大きく左右することになります。また、会社規模が大きくなればなるほど、人事業務の複雑性は指数関数的に増していきますので、ますます仕組みとして整っていなければなりません。そのため、手続きや情報の管理を効率的に行い、組織設計、採用、人材育成、離職対策など有効な施策立案に繋げ、人事の価値創出に寄与することがHRTechサービスに求められる役割です。
しかしながら、既存のサービスは機能によって(採用管理・勤怠管理・労務管理といったように)分類化されており、人事業務を包括的にカバーできるものはありません。 例えば、「採用・人材育成・離職対策」といった機能は相互に関係性を持ちますが、それぞれの機能ごとにプラットフォームが分断されていれば、統合された情報を元にした適切な人事オペレーションを提供することは難しくなります。 既存の人事業務を各機能ごとにデジタル化(またはDX化)することはできても、HRTechを利用することで人事の価値創出に繋げることができていないということが、人事業務とHRTechという領域の課題といえます。
前述の課題に対して、私たちのサービスでは例えば、各人事業務で蓄積された従業員の志向性やエンゲージメント、実績や評価などの情報を元に個別適切な配置転換や離職対策に活用することができます。 昨今の潮流としても、企業は労働者の価値観やワークスタイルニーズの多様化などにより、以前よりもパーソナライズされた労働環境の提供が求められています。 より個に適した形で人事業務を行うには、分断されたプラットフォームではなく、包括的に情報を管理できる環境が必要になります。
私たちの事業部ではマイクロサービスアーキテクチャを採用しており、開発体制としてはスクラムを採用しています。またスクラムの中でもLeSSと呼ばれるものに近く、4-6人程度を1群としたチームが4チームほどで動いています。それぞれのチームが1つのプロダクトを開発するよう自己組織化されています。
意思決定の速さ 事業部としての大きな方針はありますが、チームのための意思決定はチーム独自で行っています。人員構成もエンジニアが主体となっているためエンジニア主導でコトが進みその速度も速いです。少人数ゆえメンバー1人1人の意見がダイレクトにチームに反映されます。
業務の範囲に制限がない 担当業務は単純な作業に留まることはなく、フロントエンドやバックエンド、インフラを問わず機能の要否や設計から実装まで行います。 作業範囲が広いため、様々な技術に触れながら成長することができますし、事業計画に直接携わるなど、エンジニアに留まらない仕事の仕方ができる可能性もあります。 もちろん作業範囲が広いだけではありません。例えばプロダクトの仕様についても、単に仕様を満たすための部分的な作業ではなく、プロダクト全体、ひいてはサービスの成長を見据えた上で設計を行うため、事業経営の中核に立って開発を進めることができます。
コミュニケーションの取りやすさ 少人数のチーム構成のため、メンバーと自分の作業関係が把握しやすく、開発に関するやりとりも齟齬なく行うことができます。 開発業務はハードですが、他のチームも含めて非常に安心感のある組織だと自信を持って言えます。
フロントエンドとバックエンドの使用言語を揃えることで、同じ文法で開発でき、実装者の学習コストを下げ、ソースコードの流用もしやすくなります。 実装者もフロントエンド、バックエンドを問わず実装に参加しやすくなり、フロントエンドもバックエンドも実装したい開発者にとっては魅力だと思います。(ごく一部PHP, pythonもありますがほとんどはTypeScriptです。)
オーバーフェッチングやエンドポイントの複雑化などREST APIの課題を解決するために生まれました。 AirbnbやNetflix、Shopifyなどをはじめとした多くの企業で使用されています。 クライアントとしてApollo GraphQLを利用しており型安全なAPI開発、利用が実現できています。
私たちのチームでは、外部サービスの利用を除きプロダクトのバックエンドインフラをGoogle Cloud Platform(以下GCP)の利用に振り切って開発しています。 Cloud RunやCloud SQLをはじめとしたマネージドサービスを利用することで、開発者はコンテナやコンテナ内部のアプリケーションの開発に専念することができます。 Cloud PubsubやCloud Build、Cloud Monitoringなども利用しており、GCPインフラでのDevOpsやSREに興味のある方にとっても魅力の高い環境かと思います。 下記に簡単に仕様技術の一覧をまとめています。
いかがでしたでしょうか。
私の所属するHRテック事業部は発足してまだ日が浅く、プロダクトの状況も0-1フェーズにあります。しかし、だからこそ今回ご紹介したような魅力を強く感じられる組織であると思っています。私自身もレバレジーズに入社してまだ1年未満ですが、事業内容や使用技術、働き方など、非常に挑戦的で魅力のある環境だと実感しています。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!
本文
レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。レバレジーズは創業以来事業の成長が続いていますが、ここ最近は事業の拡大にシステムの開発が追いつけていない状態でした。そのため、システム開発が事業の拡大に追いつき、更には加速させるためにマイクロサービス化を主体においた技術スタックの刷新を進めています。この記事では、導入したまたは導入しようとしている主な技術の紹介と、選定した技術が目先の開発だけではなくエンジニア組織全体の課題をどのように解決するかを紹介していきたいと思います。
まず、現在導入したまたは導入しようとしている主な技術のご紹介から始めます。
列挙した技術がざっくりどの領域に関係するかの図はこちらになります。
こちらに紹介しているのは主要な技術のみで、他にも様々な技術検証、導入を進めています。それらはこれからの弊社の技術者ブログを楽しみにしていただければと思います。また、個別の技術の紹介もこちらの記事では詳しく行いませんので、あしからず。
それでは、本題の技術導入で解決したい課題を説明していきます。
まず弊社の開発体制ですが、基本的には1サービス毎にチームが付き、サービス毎にコードベースを分けて開発を行ってきました。
レバレジーズ主力事業である『レバテック』は、サービスを使ってくださるエンジニアの方や、企業様が年々増えており順調に成長を続けてきました。また、『レバテックダイレクト』や『レバテックカレッジ』をはじめとするレバテックシリーズの新サービスや、看護師転職支援サービス『看護のお仕事』、ITエンジニア向けQAサイト『teratail』などのサービスも提供をしてきました。しかし、開発サイドでは、長年機能とビジネス範囲の拡張を続けたことによるコードベースの肥大化や、サービス間のデータ連携のために本来独立しているはずのチームが密に結合してしまい、人を増やすだけでは事業成長に追いつけなくなってしまいました。特に
の3つによって、組織としての開発効率が下がってしまっていました。
モノリシックサービスとは、一つのプログラムで一つのサービスを作ることを指します。
弊社もこれまではモノリシックにサービスを開発してきましたが、長年機能拡張を続けてきた結果、PHPで開発してきたことも相まって、コードを一部修正した場合に影響範囲がどこまで及ぶがわかない状態となり、影響の確認のために多大な工数がかかるようになってしまいました。また、サービス間でのデータ連携も行われているため、影響範囲が1つのサービスだけに留まらず他のサービスを巻き込んでバグを出すということも頻発するようになってしまっていました。
さらに、一つのプログラムが受け持つビジネス領域も拡大してきた結果、必要な業務知識も膨らみ、新しいエンジニアがまともに開発できるようになるまでにも多くの時間がかかるようになっていました。
弊社では基本的に1サービス1チームが割り当てられ、コードベースも分けて開発を進めています。しかし、これまではコード規約ぐらいしか標準化されたものが無く、開発スタイルや設計、インフラ構築方法もチームでバラバラでした。そのため、スポットで他チームへヘルプに入ったり、ノウハウの共有が困難でした。
新規サービス開発の際も、機能の流用ができる作りになっていないので、各サービスで同じような機能をバラバラに作るということも多く発生していました。
そのため、チームが増えても相乗効果が発生せず、組織としての開発効率がいまいち上がらない状態になっていました。
CI/CDが入っていないサービスも多々あり、デプロイやインフラ構築もシェルを手動実行するものが数多く残っていました。そのため、開発面ではチーム毎、開発者ごとにコードの品質が揃わなかったり、運用ではデプロイが限られた人しか実行できなかったり、新しいサービスを作る際にもインフラ構築に工数が多くかかる上に、インフラの監視やログ監視も抜け漏れが発生するなど、開発効率の低下や品質の低下を招いていました。
上記の課題を解決するのは一つの技術だけでは解決できないため、マイクロサービス化を中心においた技術スタックの刷新を行い、技術の組み合わせによって解決を目指しました。
プログラムの設計において分割統治という考え方があります。大きな問題を小さな問題に分割して、小さな問題をすべて解決することで全体を解決するという考え方です。 サービスのフロント、BFF、バックエンドの3層への分割、ビジネスドメインを再整理しドメインに分割しマイクロサービス化、一つのマイクロサービス内でのレイヤードアーキテクチャによる分割を行っています。それにより、
などのメリットを享受でき、結果、長期に渡り開発効率を高い状態で維持することが期待できます。
マイクロサービス化、レイヤードアーキテクチャ化することでチームは細分化されることになるので、チーム間のコミュニケーションが重要になってきます。そのため、フロントエンド、バックエンドともにTypeScriptへ切り替え、マイクロサービス間はgRPC、フロントエンドとバックエンド間はgRPC-webまたはGraphQLを導入しました。gRPC、gRPC-web, GraphQLのIDL(interface definition language)が、そのままAPI仕様書となり、IDLからコードを自動生成しコンパイルを行うことで変更点をコンパイルエラーとして検出できるため、仕様変更した際の伝達が容易かつ安全に行えるようになります。
レイヤードアーキテクチャ化によるユニットテストの容易化、TypeScript化によるコンパイルエラーによるエラー検知を活用するためCI/CDとしてgithub actionsを導入しソースコードのPush時に自動で検証できるようにしています。
TS化、CI/CDでエラー検知が容易なったことでコード生成も最大限活用できるようになりました。gRPCのIDLからのコード生成の拡張によりレイヤードアーキテクチャのためのボイラープレートの生成や、DBアクセスオブジェクトからの安全なSQL発行コードの生成などを導入することで、開発速度の向上に加えコード品質の安定化も行っています。
一部のチームではユニットテストに加えてcucumberを導入して、マイクロサービスのAPIをBDD(振る舞い駆動開発)、フロントエンドをATDD(受け入れテスト駆動開発)しています。実装前にエンジニア同士やディレクター-エンジニア間で仕様のすり合わせと検討が可能になり、手戻りが減った上に設計品質の向上にも繋がりました。
弊社はAWSをメインに使用しているため、インフラの構築はCDKを導入しました。CDKはTypeScriptやJavaなどプログラミング言語を使ってインフラを構築できる技術です。アプリサーバーの構成がすべて、フロント、BFF、バックエンドの3層化しDocker+ECS Fargateでサーバーレス化したことで、同じインフラ構築のコードをどのチームでも使い回せるようになりました。CDKをラップした社内ライブラリを用意したことで、アプリエンジニアがインフラ構築から行えるようになり、かつ、サーバーの構築が共通化されたことで監視の漏れや設定のミスもなくなりました。
現在は更にDataDogの導入も行い、インフラ監視、ログ監視などを強化しています。
また、マイクロサービス化が進み、サーバーも増えてきているため、ServiceMeshも導入を進めており、より緻密で効率的なサーバー管理を目指しています。
技術選定したはいいものの浸透しないということもあると思います。大体は新しい技術を覚えたり切り替えることに対する不安が原因なことが多いです。今回選定した技術に関してほぼ全て社内ライブラリを用意し弊社の開発に特化させることで、
新しいことを覚えるコスト <<< 選定された技術を使うメリット
という状況を作り出しました。一つの技術選定だけでは実現は難しいですが、複数の技術をうまく組み合わせその状況を作り出せれば、あとは自己増殖的に浸透してくれます。
レバレジーズは「関係者全員の幸福を追求する」ミッションを達成するため、今回ご紹介したように、開発効率を高め、事業実現を素早く行えるエンジニア組織への変革をすすめています。そのため技術選定も技術的好奇心や目下の開発だけにとらわれない選定を心がけて行っています。しかし、まだまだ改善できるところは多々あります。もし「技術選定を行って組織を変えていきたい」と思っている方がいましたら、ぜひ一緒に働きませんか?エキサイティングな仕事ができると思います。 ご興味ある方は、こちらからエントリーをお願いします! https://recruit.jobcan.jp/leverages/recruit.jobcan.jp