TECH PLAY

株式会社スタンバイ

株式会社スタンバイ の技術ブログ

64

2024年8月30日(金)台風10号の予報が出る中、 普段は静かなスタンバイ本社がいつもと違う熱気に包まれました。 8月27日(火)〜29日(木)の開発期間を経て、株式会社スタンバイ「ハッカソン2024」の最終プレゼン大会が開催されました。 開発期間は8時間×3日の24時間、社内公募により集まった8チーム計34名のメンバーが入賞を目指して準備しました。 開会式の様子 開会式では開催概要のほかに、審査員をChatGPTが解説する審査員紹介が披露され、 氏名の読み方など、細かなミスを除けば完璧な説明精度に、参加者から「おーっ!」と歓声があがりました。 COOの名前は「やまもと さとし」、社名は「Stanby」なんだけど・・・ 【ハッカソン2024のテーマ】 今回のハッカソンのテーマは、「時代とともに変わる『検索』に求人領域で向き合う」。 GenerativeAIを活用してそれぞれのチームが仕事探しに関わる課題を見つけ、解決につながるアイディアを出し合い、実機でデモができるアウトプットを競いました。 【★参加チーム名(チーム名の由来)と課題 / ソリューション】 ★一辻二鷹三細び(メンバーの頭文字を縁起の良い形式で) 課題:仕事探しへのアドバイスを受けられる人はなかなかいない ソリューション:自分の想いを自由入力+選択形式の超簡単フォームに入力するだけで、最適な仕事探しができる ★NA・KA・YA・MA軍団(中山リーダーの名前由来かと思ったら、メンバー全員の頭文字から) 課題:検索リテラシーが低い、自身の仕事の適正がわからないミドルシニア層を救いたい ソリューション:履歴書ファイルをアップロードすれば、適正のある仕事が提案される ★寝袋持参プログラマー(過去に寝袋持参して働いていたエンジニアだから) 課題:お金のためではなく、生きがいを見つけるために働く人を増やしたい ソリューション:人の感情に訴えられる求人情報を、AIを駆使して自動作成する   ★未来職道(AIに考えてもらった名前から) 課題:求人検索だけではなく、転職成功まで支援するサービスを求めているユーザーが多い ソリューション:LLMを通じて性格にあった業種を紹介し、就職までのリスキリングから支援する ★断罪の技術錬成者 ξ虚空に響くアルゴリズムξ(AIに考えてもらった名前だが、「ξ」は人力) 課題:異業種異職種への転職を支援する ソリューション:新しい職種を検索した際に、気付けていない条件を見つけて精度の高い検索をサポートする ★fan-LON-KUIN(メンバー頭文字の羅列から) 課題:仕事探しの潜在的な条件を言語化できない ソリューション:ユーザーが見た求人情報をもとに、新たな検索クエリを提案する ★HMMS(メンバーの頭文字の羅列から) 課題:転職のハードルが高く、現状に我慢している人が多い ソリューション:AIによる対話形式で、職歴/スキル/条件等をヒアリングしながら仕事の提案やアドバイスを行う ★なかのひとたち+a(メンバーのほとんどがハッカソン事務局メンバーだから) 課題:自分に合った仕事の探し方がわからず、手間がかかる ソリューション:AIとの音声会話だけで強みが引き出され、面接まで手軽に進める 【第1部:プレゼンテーション】 8つのチームが、3分という限られた時間の中でプレゼンテーションを行いました。自分たちが考え抜いたアイディアがなぜ実現されるべきなのかについて、言葉とスライドで熱弁しました。中には3分で語り切れずに、「もっと伝えたいことあったのに、、!」と悔しがりながら終了してしまうチームや、スライドにデモンストレーション動画を挿入しスムーズに進めるチームなど、各チームの想いが前面に出たプレゼンテーションとなりました。 「めっちゃ未来があります!」と締めくくる 求人業界の罪を背負うのではなく、断罪したいという思いを 【第2部:展示セッション】 展示セッションでは8つのブースにチームが配置され、 審査員や観客がチームブースへ訪問し、改めてサービスの紹介やより細かなデモンストレーションを確認しました。 第1部のプレゼンテーションでは確認しきれなかった、実際のデモ画面の動きや技術的な工夫・こだわりが垣間見えて「面白い着眼点!」「これは事業ロードマップに入れたいよね」といった声が審査員から挙がり盛り上がりました。 時間内に全てのブースを周りきれなかった人や、今回の発表参加者からも「他のブースも気になるので行きたいのに・・・」などの声が出るほど、興味深い内容が多く発表されました。 【表彰】 プレゼンテーションと展示セッションのデモンストレーションを終えて、いよいよ表彰グループの発表。表彰されたチームとコメントを紹介します。 CTO賞:なかのひと+a 課題:自分に合った仕事の探し方がわからず、手間がかかる ソリューション:AIとの音声会話だけで強みが引き出され、面接まで手軽に進める 評価ポイント ・選考プロセスを簡略化するという課題解決に着目している点が目を引いた ・クリアしなければいけない技術的課題があるものの、将来的にチャレンジしたいと感じた内容であった LINEヤフー執行役員賞:寝袋持参プログラマー 課題:お金のためではなく、生きがいを見つけるために働く人を増やしたい ソリューション:人の感情に訴えられる求人情報を、AIを駆使して自動作成する   評価ポイント ・求人を作る企業側に着眼点を持っていた点がユニーク ・求人のテキスト生成は、他社でもやろうとしており実現性もクリア ・画面生成部分まで検討しており、色々なバリエーションを作って企業に選んでもらう点も面白い 最優秀賞・COO賞:NA・KA・YA・MA軍団 課題:検索リテラシーが低い、自身の仕事の適正がわからないミドルシニア層を救いたい ソリューション:履歴書ファイルをアップロードすれば、適正のある仕事が提案される 評価ポイント ・非常にシンプル、レジュメを入れるだけで自分にあった求人が提案される点がUX観点で優秀 ・検索をしない世界をどう作るのか、ユーザーの情報をどう取得するのかという課題を解決するアプローチで、事業ロードマップにフィットさせられる 【総括】 CTO明石: 「受賞したかどうかに関わらず、サービスに取り込みたいアイデアがたくさんありました。最優秀賞の企画は今後のロードマップに載せて取り組むので、「NA・KA・YA・MA軍団」以外の皆さんにも関わってもらうプロダクトになります。今後も、今日のような機会で出てきたアイディアをどんどん世の中に出していきます。 事務局メンバーも急遽始まった企画だったにも関わらずお疲れ様でした。予算は少なかったですが、DIY感が出て良いイベントにしてくれました。次回は予算を確保して豪華にやりたいので期待していてください。 今朝、オフィスの雰囲気が凄くよかった!グループごとに固まって議論していて、賑やかな雰囲気だった。こういう機会を増やしてチームワークをより強化していきたいので、引き続きみんなで頑張っていきましょう!」 【ハッカソンを終えて】 スタンバイとしての初めてのハッカソンでした。 終えてみての感想をハッカソン参加メンバーに聞いてみると、 「普段の業務では接点がなかった人達と関わりを持てた」 「通常業務から一歩離れて、1からみんなで考えて作ることを楽しめた」 「他の人の考えや技術、スタンバイへの熱量を感じることができた」 など、日々の業務では体験しづらい刺激を受ける良い機会になりました。 また、同じ「仕事探し」というテーマでも、検索/求人作成/選考フロー/リスキリングなど、さまざまな課題に向き合いました。その中で最も多かった「検索」の中でも、情報の入力方法について各グループが多様な発想で考えて、アイディアが豊かに飛び交った時間でした。 そしてなにより、ハッカソンに参加したメンバーの充実した表情と「第2回があったらまた参加したい!」といった声を多数聞けたことが印象的で、今後のスタンバイの文化を象徴するイベントになる予感がしました! 企画から運営までを担当した事務局メンバーのみなさん、ありがとうございました。 第2回のハッカソン開催に、乞うご期待! スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
初めまして、スタンバイのソフトウェアエンジニアを務めておりますの一般エンジニアです。 ChatGPTの流行により大規模言語モデル (LLM) が注目を集める中、生成AIの開発はさらに活発化しています。その中でも注目されているのが、RAG (Retrieval Augmented Generation) です。これは、外部ソースから関連情報を検索し、生成されたコンテンツに組み込むことで、より情報量が多く正確なテキストを生成する手法です。 本記事は、今年初めに社内向け作成したLLMとVespa活用プロトタイプの内容を再構成したものです。当時の Vespa 最新機能 (2024.03 時点) を用いてLLMの可能性を検証するデモでしたが、生成AIの開発は日々急速に進んでいるため、記事内容は最新の情報を完全に反映していない可能性があります。ご了承ください。 背景 Vespa は、検索やレコメンドなど、ハイパフォーマンスなサービングユースケース向けに設計された、オープンソースのサービングエンジンです。テキスト検索、ベクター検索、マルチフェーズ検索やランキングなどを含むハイブリッド検索など、最先端の機能を備えています。Vespaの詳細は、 公式サイト をご覧ください。 スタンバイでは、検索エンジン基盤としてVespaを採用しています。Vespa移行の詳細については、下記の同僚の 記事 をご覧ください。 プロトタイプアイデア スタンバイは、コアとなる 求人検索エンジン だけではなく、求職関連メディアサイト 「スタンバイPlus」 も運営しています。「スタンバイPlus」では専門家によるレビュー・編集済みの求職者向けの高品質なアドバイス、各種職種紹介など、様々なコンテンツを提供しています。 生成AIとサイトコンテンツを組み合わせれば、完全にセルフサービスのAI求職アドバイザーへと変貌し、求職者の疑問や不安に役立つ回答を提供できる可能性があります。 プロトタイプ 「教えて!スタンバイ太郎先生」 という名前のプロトタイプを開発しました。 このプロトタイプは以下のような機能を提供します: LLM 生成による質問に対する RAG レスポンス Vespaによる多言語セマンティック検索 LLMによる求職関連クエリの提案 質問に関連したスタンバイの求人推薦 プロトタイプの開発に使用したスタックは以下の通りです: 検索エンジン: Vespa LLMフレームワーク: LangChain 基礎モデル: Anthropic Claude 3 Haiku via AWS Bedrock Web App: Mercury プロトタイプの全体的なフローは下記の図のようになっています: 以降のセクションでは、主要部分のデザインの詳細について説明します。 ドキュメント処理 スキーマ まず、検索エンジンのスキーマを定義します。 今回はプロトタイプなので、検索用の最低限のフィールドのみ定義します。 schema article { document article { field title type string { indexing: summary | index index: enable-bm25 } field body type string { indexing: summary | index index: enable-bm25 summary : dynamic } field path type string { indexing: summary | index } } field embedding type tensor<float>(x[768]) { indexing { "passage: " . (input title || "") . " " . (input body || "") | embed e5 | attribute | index } attribute { distance-metric: angular } index: hnsw } field colbert_embs type tensor<int8>(token{}, x[16]) { indexing { (input title || "") . " " . (input body || "") | embed colbert | attribute } } fieldset default { fields: title,body } } フィールド スタンバイPlusの記事は、HTML形式からマークダウン形式に変換され、bodyフィールドに格納されます。 Embedder 新しいバージョンのVespaでは、Vespaクラスターのドキュメントプロセッサ内で直接実行されるEmbedderコンポーネントが導入されました。ドキュメントがVespaに投入されると、このプロセスで自動的にEmbeddingが生成されます。Vespaクラスター内で直接実行されるため、別のベクトル化APIを管理してEmbeddingを作成するための手間とコストが少なくて済みます。 このプロトタイプでは、ドキュメント投入時に2種類のEmbeddingを作成するための2つのEmbedderを定義しました。 1つは検索用のE5 Embedding、もう1つはColBERT Embeddingです。 Embedderは、Vespaクラスターのservices.xmlアプリケーションパッケージファイルに数行追加するだけで定義できます。 <component id = "e5" type = "hugging-face-embedder" > <transformer-model url = "/multilingual-e5-base/model.onnx" /> <tokenizer-model url = "/multilingual-e5-base/tokenizer.json" /> </component> <component id = "colbert" type = "colbert-embedder" > <transformer-model url = "/colbert/model.onnx" /> <tokenizer-model url = "/colbert/tokenizer.json" /> </component> URLは説明のためのものであり、実際に使用するURLとは異なります。 Vespa Embedderコンポーネントの詳細については、Vespa の エンベッディングドキュメント を参照してください。 ハイブリッド検索 このプロトタイプでは、従来通りの二段階ランキングを採用しました。 マッチング マッチングでは、Vespaでネイティブにサポートされている近傍ベースセマンティック検索(Nearest Neighbour Search)とレキシカル検索(Lexical Search)を組み合わせ利用します。(ハイブリッド検索として知られる) 近傍検索のほうがレキシカル検索より優れているのかという議論がよくありますが、レキシカル検索は時代遅れで退役すべきという意見もあります。実際には、レキシカル検索と近傍検索にはそれぞれ長所と短所があり、両方の検索結果を組み合わせることで、両方のメリットを享受できます。 Vespaの得意な点としては、1つの検索リクエストで同時に両方検索できるため、効率的で手間が少なくて済むことです。 Vespaのランクプロファイル関数内で、ファーストフェーズで直接同時検索できるように定義すると、ハイブリッド検索を簡単できます。 first-phase { expression: nativeRank + closeness(field, embedding) } nativeRankスコアはレキシカル検索からのテキストマッチングスコアであり、closenessはクエリとドキュメントのEmbedding間の近さを表します。 各検索からのスコアに重みを付けることもできますが、このプロトタイプでは最適化を行う時間がなかったため、単純に2つのスコアを加算しています。 このプロトタイプでは、デモ目的ですべてが同じマシン上で実行されているため、Embeddingモデルをファインチューニングする時間とリソースがありません。そのため、パフォーマンスと簡便性を考慮して、 intfloat/multilingual-e5-base テキストEmbeddingモデルを使用しています。 プロトタイプで使用したVespa検索エンジンの実際のリクエストは次のようになります。 { " yql ": " select title, body, path from article where (({targetHits:10}nearestNeighbor(embedding,q)) OR weakAnd(userQuery())) ", " ranking ": { " profile ": " colbert " } , " model ": { " locale ": " ja " } , " hits ":" 5 ", " query ":" 在宅ワークは何の仕事ですか? ", " input.query(q) ": " embed(e5, \" query: 在宅ワークは何の仕事ですか? \" ) ", " input.query(q_t) ": " embed(colbert, \" 在宅ワークは何の仕事ですか? \" ) " } このプロトタイプでは、Vespa Embedderコンポーネントを使用して、リクエスト時にユーザークエリをEmbeddingに変換し、近傍検索に使用しています。 リランク リランクステージでは、プロトタイプの目的の1つとして、最新のVespa機能を実証することだったので、ここでColBERTを使用することにしました。 ColBERT (Contextualized Late Interaction over BERT) は、効率的で効果的なドキュメント検索のために設計されたニューラルランキングモデルです。クエリとドキュメントの両方を BERT を使用して密なベクトルにエンコードします。個々のトークンEmbedding間の類似度スコアを計算することで、シーケンス全体ではなく、レイトインタラクションを実行します。 レイトインタラクション(Late Interaction)は、検索クエリとドキュメントを個別に処理し、プロセスの最終段階まで相互作用を遅らせることで、効率的で正確な検索を実現します。検索クエリとドキュメントの表現は、それぞれ独立してエンコードされ、その後相互作用が行われるため、レイトインタラクションと呼ばれています。 これにより、効率性を維持しながら、細かいマッチングが可能になります。ColBERTは、リランキングステージで使用できます。ファーストフェーズで検索された候補ドキュメントをより深い意味的な理解に基づいてランキングの精度を向上させることができます。 残念ながら、Vespa自体はColBERTで使用される類似度スコアを計算するためのMaxSim関数を提供していませんが、Vespaが提供する算術演算子を使用してカスタム関数を作成できます。 以下は、リランクフェーズのランクプロファイルのスニペットです function maxsim() { expression { sum( reduce( sum( query(q_t) * unpack_bits(attribute(colbert_embs)), x ), max, colbert_embs ), q_t ) } } second-phase { expression: maxsim() } リクエスト時に変換されたクエリEmbeddingと、フィーディング時に作成されたドキュメントEmbeddingが、セカンドフェーズで使用され、意味的類似度スコアを計算します。計算されたスコアに基づいて結果がソートされます。 レスポンスの生成 LLM Vespaから記事検索結果を取得した後、これらの記事がRAGのAG(Augmented Generation)部分で使用されます。 ファウンデーションモデルは、AWS Bedrockを介してAnthropic Claude 3 Haikuを使用しました。Claude 3 Haikuは、Claude 3 ファミリーからコンパクトなサイズで即時応答用に設計されたモデルです。個人的なテストは、Haikuを、SonnetやOpusなどのより大きなモデルを使用して生成された応答と比較して、RAGタスクに対して十分であることがわかりました。AWS Bedrockを選択した理由は、スタンバイは主にAWSインフラを使用しているからです。実際には、日本語をサポートしていれば、他のLLMモデルやプロバイダーと置き換えることができます。 プロンプト このプロトタイプで使用されたプロンプトは、時間の制約により、プロンプトの最適化が行われていません。 プロンプトは日本語ではなく英語を使用しています。他のLLMプロトタイプや内部使用ツールを開発した経験から、プロンプトは日本語ではなく英語を使用しています。Claude 3モデルは日本語のプロンプトの指示を理解できますが、英語のプロンプトの方が定量的および定性的な分析において、日本語よりも一般的に優れた応答を提供するためです。そのため、ここも主に英語をプロンプトに使用しています。 プロンプト自体は、検索結果を格納する {context} と、ユーザークエリを格納する {query} の 2 つの変数のみを受け取ります。以下は、このプロトタイプで使用された実際のプロンプトです。 You are a helpful, precise, factual Japanese speaking Job Seeking Advice expert who answers questions and user instructions about Job Seeking-related topics. The documents you are presented with are retrieved from a Japanese Job Seeking Advice Blog called Stanby Plus (スタンバイPlus). Facts about Stanby Plus: - Stanby Plus is a magazine website operated by Stanby Inc. which operate a Japanese Job Search Engine Services called Stanby (スタンバイ). - Stanby is a focus on the Japanese market. <instructions> - The retrieved documents are markdown formatted text from a Japanese Job Seeking Advice Blog operated by Stanby, a Japanese based Job Search Engine company. - Answer questions truthfully and factually using only the information presented. - If you don't know the answer, just say that you don't know, don't make up an answer! - If you can't answer with reference to the document provided, just say 「大変申し訳ございません。検索されたクエリが適切的に答えができまん。」 - You are correct, factual, precise, and reliable. - You must reply in Japanese. - You should reply in less than 500 words. </instructions> <articles> {context} </articles> Question: {query} Helpful factual answer: プロトタイプは Jupyter Notebook 上で直接実行されるため、LangChain を使用して AWS Bedrock を通じて LLM とやり取りしています。ウェブアプリ自体は、Jupyter Notebook をウェブアプリに変換する Python ライブラリであるMercuryを使用して構築されています。 また、LLMを使用してユーザーの質問から求人検索クエリを作成し、求人推薦を表示する試みも行いました。LLMは、このサブタスクでも良い性能を発揮しました。 デモ プロトタイプは内部デモ用なので、公開はしていませんが、プロトタイプのスクリーンショットをいくつか紹介します。 生成された応答のなかで、スタンバイの求人検索サービスを自然に推奨しています 👍 他の言語で質問された際に、質問に利用された言語でそのまま返答します 👍 結論 Vespaが提供する高度な機能により、このRAGプロトタイプは1日以内で作成できました (フロントエンド言語に慣れていないため、UIの作成に多くの時間を費やしました)。これは、LLMモデルとVespaの可能性を示しています。 プロトタイプの外部公開までまだ長い道があります。以下のようなアイディアでさらなる改善を検討しています。 より良いリトリーバルのために、e5 モデルを当社のデータでファインチューニングする より良いColBERTモデルを学習する (現在もColBERTをまだ実験中) プロンプトの悪用防止対策 (このプロトタイプは内部使用のため、これについては何もしていません) 生成されたコンテンツに、その基になっているものを記載します スタンバイでは、常に新しいアイデアや技術を調査し、試しています。新しいことに挑戦したい方や、素晴らしいプラットフォームで素晴らしい仲間と仕事をしたい方は、ぜひ 採用ページ をご覧ください! スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
こんにちは、株式会社スタンバイのSearchグループで検索エンジンの運用・開発を担当している小野です。 今回は、社内で実施した「検索システム」の輪読会についてご紹介します。 なぜ輪読会を行ったか? 今回、輪読会を開催した理由は大きく2つあります。 検索サービスを提供する企業として、検索機能に関する体系的な知識を深めたい ディスカッションを通じて、検索システムに関する理解をさらに高めたい 私たちが使用したのは、ラムダノート社の 『検索システム』 という書籍で、検索システムの開発に必要な体系的知識を学べる一冊です。 スタンバイが提供する求人検索サービスのシステム規模はとても大きいため、コンポーネント別に担当チームが分かれて開発や運用を行っています。 輪読会では業務での担当以外を含む検索システム全体の理解を深め、共通の知識基盤を築くことで今後のプロダクト開発にも役立てたいと考えました。 ちなみに、スタンバイには必要な書籍の購入補助してもらえる「スタンバイ図書」という制度があります。 今回も輪読会実施のために制度を利用して複数冊購入してもらいました。 どのように実施したか? 進め方 週に1回、1時間オンラインで行い1章ずつ進めました。 全12章を約3ヶ月で完了することを目標にしました。 参加メンバーには事前に該当章を読んでもらい、輪読会ではその内容を共有しディスカッションを行う形式です。 進行は以下のステップで進めました。 学んだ内容や疑問点をふせん(miro)に記入 そのふせんを参加者全員に共有 深掘りしたい内容についてディスカッション 章ごとにmiroで以下のようなボードを作成しました。 工夫したこと 輪読会の価値を最大化し、参加しやすくするために以下を工夫しました。 キックオフの実施 1回目の輪読会の開催前にキックオフを行って、参加者の輪読会へのモチベーションの共有や進め方を決定しました。 キックオフで輪読会を「どういった場にしたいか?」の認識合わせをできました。 個人の負担軽減 要約を作成して発表する形式は取らず、各メンバーが自分のペースで参加できるようにしました。 ファシリテーターは各回で持ち回り制とし、特定のメンバーに負担が集中しないよう配慮しました。 本は事前に読んでくる 輪読会ではディスカッションに重点を置くために、会の中で読書や音読するのではなく事前に本を読んできてもらうようにしました。 実際のところどうだったか? 各回で約7名が参加し、全12章を無事に読破しました! 上手くいったこと 輪読会では書籍の内容から発展して実際のプロダクトについての議論ができたことです。 多様なバックグラウンドを持つメンバーが参加していたので各人の業務での知識や経験に基づいて議論は非常に有意義でした。 例えば、新しく入社したメンバーが「スタンバイではどうなっているのか?」という疑問を投げかけ、経験豊富なメンバーがそれに答える場面が多く見られました。 社内での輪読会だからこそ業務に直接リンクする内容まで深掘ることができました。 難しかったこと 章によって難易度やページ数にバラツキがあり、各章を1時間内に収めるのが難しかったです。 特に後半の章ではアルゴリズムや機械学習といった専門的な内容が増え、時間内に深い議論をすることが難しくなりました。 章の内容によっては会を分割したり、サンプルコードを使ったモブプログラミングなど、異なる進め方を採用しても良かったと感じました。 終わりに 今回の輪読会を通して、個々のスキルアップはもちろん、メンバー間で共通の知識基盤を構築し、会社全体のプロダクト開発力を高めることができました。 忙しい業務の中で参加してくれたメンバーには心から感謝しています。 今後も、メンバーと共に成長し続けられる環境を作っていきたいと思っています! もしスタンバイについて少しでも興味を持った方は、ぜひ 採用サイト からお気軽にお問い合わせください。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに こんにちは。株式会社スタンバイ QAグループ(Quality Assurance Group)の樽井です。 スタンバイは求人検索エンジンを開発・運用しており、Webとネイティブアプリ(以降App)でサービスを提供しています。Web・Appのテスト自動化にはMagicPodを利用していますが、本記事では、AppのMagicPodシナリオ 1 失敗率(シナリオの不備による失敗)を下げるための取り組みをご紹介します。 magicpod.com MagicPod導入の経緯については、過去記事「 スタンバイ QAのテスト自動化導入(MagicPod) 」をご確認ください。 なぜ失敗率を下げるのか AppでMagicPod運用を始めた頃のシナリオ失敗率(失敗数※ ÷ シナリオ数)は、Androidが16%前後、iOSが44%前後でした。 ※環境やデータなどシナリオ内容以外の失敗数を除いた数 スタンバイでは、テスト自動化の専任者を立てず、Web、AppそれぞれのQA担当者がシナリオ運用を実施しています。 一日1時間前後をMagicPod運用作業に充てていますが、既存シナリオの失敗率が高いことで、修正に時間が取られ、新規シナリオの作成が進まなかったり、新しい機能の反映が遅れたりする影響がありました。加えて、iOSはAndroidに比べて実行速度が遅く、シナリオ修正後の確認にも時間を要します。1つのロケーター 2 を確定させるのに15分ほどかかることもありました。 加えて、失敗が多いシナリオでは、不具合へ辿り着く前にシナリオが終了してしまい、十分な品質担保ができません。 そこで、シナリオを安定的に実行し、必要な機能実装を進められるよう、失敗率を下げる取り組みを行うことにしました。 MagicPod実行ログから何がわかったか まずは、シナリオ失敗理由を探るため、Android、iOSそれぞれ過去50回分のMagicPod実行ログを集計し、機能ごとに分類したシナリオの割合を算出しました(シナリオ分類)。さらに、シナリオ分類ごとの平均失敗率を算出し、失敗傾向を大まかに確認しました(シナリオごとの平均失敗率)。 OS シナリオ分類 シナリオごとの平均失敗率 Android iOS Androidの特徴 特定の機能が繰り返し失敗している 文字入力できていない 画面遷移後にタップできていない iOSの特徴 様々な機能で失敗している 画面遷移後にロケーターを取得できていない 画面遷移後にタップできていない 指定した時間内に画面描画できていない Android・iOS共通に発生していた「画面遷移後にタップできていない」という問題ですが、画面の要素が変動する場合、変動値を含むロケーターを利用していると、データにより要素数が変わることや、スクロール位置によって想定していない要素を操作することがあります。変動値を含むロケーターの方が使いやすいシーンもありますが、固定されたロケーターを利用することで、「XXXが表示されるまでスクロールする」や、「XXXが存在するか確認する」などのコマンドが利用しやすくなります。 データ量やスクロール位置の変動による、ロケーター取得の失敗は、シナリオ作成〜実行〜運用の全てにおいて手間と時間がかかることから、「安定したロケーター取得」を最優先の課題として取り組むことで、失敗率の安定を図りました。 ロケーターを安定させるにはどうすれば良いか〜Accessibility ID〜 そんな折、MagicPodのヘルプセンターで見つけた記事「 iOSアプリのテストを高速化するロケーターの選び方 」でAccessibility IDの存在を知りました。ロケーターとしての使い勝手の良さとしては、iOS Class ChainやXPathに軍配が上がりますが、今回は流動的な要素ではなく、固定要素に対しての識別を目的としていたため、Accessibility IDの付与を検討しました。 Accessibility IDを付与することで、各UI要素を一意に指定できます。これによりMagicPodから要素を特定しやすくし、ロケーター取得失敗やタップ失敗を防ぐのに役立ちます。iOS、Androidで付与方法が異なり、特にAndroidの場合はUIがViewベースか、Composeベースかにより、IDの割り当て方法が異なります。詳しくは、MagicPodヘルプセンターで見つけた記事「 自動テストを簡単にするためのアプリ実装の工夫を知りたい 」に記載がありますので、ご覧ください。 事例 記事に記載があるように、Accessibility IDを利用する上での最大のポイントは、開発者にIDを付与してもらう必要があることです。いつ、どのタイミングで、どのように付与するのか、そもそも付与して良いかをApp開発チームのPOに相談することから始めました。開発者に向けては、現在の問題点とAccessibility IDに期待する点についての説明会を実施した上で、ID付与を開始しました。 Accessibility ID付与の流れ App開発チームのPOに相談 App開発者に説明会 開発担当者とQA担当者でAccessibility IDの決定 実装1(開発者) 付与内容の確認とフィードバック1(QA) 実装2(開発者) 付与内容の確認とフィードバック2(QA) MagicPodシナリオ反映 ロケーターの変化 ID付与前後でロケーターがどのように変化するのか、TextFieldを例にして記載します。もともと、iOS Class Chainで変動的なロケーターを指定していましたが、Accessibility IDを付与したことで、ID指定やIDを起点としたiOS Class Chainを使用できます。これにより、要素を明確に表示させた状態で操作を実施することが容易になります。 Accessibility ID付与前 -ios class chain=**/XCUIElementTypeTextField[1] Accessibility ID付与後 accessibility id=input_form_abc または -ios class chain=**/XCUIElementTypeTextField[`name == "input_form_abc"`] 失敗率はどう変化したか Accessibility ID付与前後で失敗率を比較したところ、大きな変化が見られました。 Android iOS iOSの場合、ID付与ありの機能を含むシナリオにて、20%を超えていた失敗率が4%台まで下がり、単純にロケーター取得が失敗することは無くなりました。1ヶ月ほど経過観察しても安定しているため、確実な効果があったと言えます。Androidの場合、iOSよりもID付与ありの機能は限定的ですが、一度失敗率が跳ね上がったものの、現在の失敗率は1%を下回っており、こちらも良い効果があったと言えます。また、Accessiblity IDを起点としたXPathの活用ができるようになり、ロケーターの可読性や安定性が高くなりました。 終わりに 今回はロケーター取得の失敗を減らすことに注力し、Accessibility IDを付与する選択をしました。失敗率を下げることで、シナリオ修正の時間を減らし、結果的にはテスト自動化運用コスト低下につながりました。実行速度を短縮したい、データパターンを活用したい、といったテスト自動化の悩みは尽きませんが、スタンバイの取り組みをどこかでまたご紹介できればと思います。 以上、ここまでお付き合いいただきありがとうございました! スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com テストシナリオは、ソフトウェアの特定の機能やフローを検証するために設定された一連の操作や条件のことを指します。シナリオは、テスト自動化ツールによって実行される具体的なステップの集まりです。スタンバイでは、「勤務地を指定して求人を検索する」といった形で、シナリオを分割しています。 ↩ ロケーターは、テスト自動化において、テスト対象のアプリケーションのユーザーインターフェース(UI)要素を識別し、操作するために使用される方法や手段です。ロケーターは、特定の要素を正確に特定し、テストスクリプトがその要素に対して適切なアクション(クリック、入力、検証など)を実行できるようにします。モバイルアプリの自動化で利用されるものに、Xpathなどがあります。 ↩
アバター
株式会社スタンバイでデザイン・フロントエンドを担当している中本です。 スタンバイではオウンドメディアとして「スタンバイplus」( https://jp.stanby.com/magazine/ )があります。 スタンバイplusでは、仕事において自分は何ができるか?私なんかでもこんなことができるんだ!という気づき・情報の提供を意識し活動をしております。 トップページのデザイン一新の背景 トップページは我々のサービスを理解するための重要な窓口と考えています。 しかし、現状スタンバイplusでは記事から入って来られる方が多いこともある為か、ただトップページが存在するだけの状態となり「トップページから記事を読ませる気が無い」のでは?と感じられるようになりました。 そこで、課題を精査し以下の点を改善することを目指しました。 全体を通じてどんな記事があるか直感的にわからない 細かい興味に沿って読み進められない 全体を通じてどんな記事があるか直感的にわからない この問題についてカテゴリの見直し(カテゴリを階層化)を行いました。 現状4つのカテゴリのみ存在し、その中に記事が分類されていました。しかし、1つ1つの分類が大きすぎるため、これだけではどんな記事があるのかわからないと感じました。 現状数百ある記事をたった4つのカテゴリに分類し、そこから興味をもつ記事を見つけるのはとても大変な作業かと考えられます。 徐々に絞り込んで探していただけるよう、新たに中カテゴリを設置し、既に存在している記事を大・中・小カテゴリへ整理し直しページ分割をしました。 それに伴い、記事一覧ページのデザインも長くただ縦に並べていたものを以下のように調整しています。 中カテゴリの一覧を設置 現状の小カテゴリの一覧 → レイアウト変更 (PCではグローバルナビゲーションから、SPではアコーディオンからもカテゴリ選択可能) トップページにおいては、記事を探す際どのようなイメージを持って記事を探せるか、各カテゴリエリアにカテゴリの特徴を伝えるように説明文を追加し、さらに細かく分類された中カテゴリへ直接遷移できるようタグを設置しました。 細かい興味に沿って読み進められない こちらの課題に関しては、解決策の1つとして前述した「全体を通じてどんな記事があるか直感的にわからない」で行ったカテゴリの見直しに(細分化)よって解決できる部分もあります。 もう1つの解決策として、トップページへ記事の掲載数増加を検討しました。 「トップページへ記事の掲載数増加」とはどういうことか? ただトップページに記事の数を増やし、現状のカテゴリを整理するだけということではなく、新たな切り口を持って読者に他にもどのような記事があるかを伝えるよう考えました。 今回新たな以下のものをトップページへ追加しました。 今月の人気記事 記事監修者 今月の人気記事 トップページにおいて、今月の人気記事を掲載することで、ほかにどのような記事が人気なのかを直感的に伝えることができるようになりました。 また、最近のアクセス数が多いものを上位に表示するため、読者にとっても興味を持っていただける記事を見つけやすくなりました。 記事監修者 記事監修者を表示することで、記事の信頼性を高めることができると考えました。 各分野での専門家が監修している記事があると言う事を知ることで、読者にとっても安心して記事を読むことができるようになります。 また、記事監修者のプロフィールへの一覧ページを設置し、監修者のプロフィールを見ることでその人物の専門性を知ることができるため、より深く記事を理解できるようにしました。 今後は、気になった監修者がどの記事を監修しているかわかるようにもしていきたいと考えています。 まとめ 今回「トップページから記事を読ませる気が無いのでは?」という疑問から、トップページから見た時どんな記事があるのか?細かい興味に沿って読み進んでもらえるようになるのか?という点を意識しました。 ただ、まだまだ改善すべき点が多くあり、コンテンツ、UI、さらにはパフォーマンス等、改善すべきところが多くあります。 今後も読者の方々にとって使いやすい、読み手を意識したメディアを目指し、改善を続けていきます。 (左:旧トップページ / 右:新トップページ) スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
株式会社スタンバイ QAグループに所属している岸です。本記事では、スタンバイの脆弱性検知の運用について紹介します。 スタンバイでは脆弱性検知のシステムとして2022年よりyamoryというツールを使っております。 参照URL: https://yamory.io/ yamoryにはいくつかの機能がありますが、スタンバイではプロダクトで利用しているソフトウェアの脆弱性検知を主として使用しています。 参照URL: https://yamory.io/service/vulnerability-management/ yamoryによる脆弱性検知の対応 【事前準備】 1.yamoryにスタンバイが使用しているGitレポジトリを登録 【運用時の対応】 1.yamoryが定期的に脆弱性データベースと照合し脆弱性の有無をチェック 2.脆弱性が検出された時にメールやSlackにて通知が行われる 3.セキュリティチームが内容を判断し、プロダクト部門に連絡 4.脆弱性の優先度により定義されたルールに従いプロダクト部門で脆弱性の内容、スタンバイでの使用方法、影響範囲を確認し、使用方法の変更、ライブラリの更新などを行い、各種自動テストや影響範囲によってはQAによる検証を通してリリースするなど必要な措置をとります。 5.対応が完了したことをセキュリティチームに報告 トリアージレベル yamoryから通知された脆弱性は社内で脆弱性の対応優先度に応じて報告ルール、対応する担当範囲、推奨する対応速度など脆弱性対応ルールが定義されております。 yamoryでは検出した脆弱性をトリアージレベルという4つのレベルが存在しています。スタンバイ社内ではこのトリアージレベルの最上位のImmediateのさらに上の区分を定義し、緊急対応として最優先対応するレベルをつけています。 参照URL: https://yamory.io/docs/auto-triage/ 脆弱性対応の重要性 対応優先度が高い脆弱性はもちろん緊急対応で対応していますが、危険度が低い脆弱性も組み合わさることでサービス運営に思わぬ影響がでる可能性もあります。スタンバイ社では危険度が低い脆弱性にも対応ルールを設けて脆弱性の撲滅に取り組んでおります。 過去に発生した脆弱性の事例では2021年のlog4jの脆弱性があり、世間的に広く使われていたライブラリなだけにIT業界を震撼させた脆弱性が記憶に残っています。 出展: Log4Shell Apache Log4jに見つかった脆弱性「Log4Shell」とは Apache Log4j の脆弱性対策について(CVE-2021-44228) 昨今のプロダクト開発において迅速で最適な開発、サービス運用を行うためにも、OSSの活用は不可欠であり、サービスの開発、運営には非常に多くのOSSを使用しています。しかし、一方でOSSの82%のコンポーネントが脆弱性やセキュリティ問題、保守性の問題など潜在的にリスクを抱えていると言われています。 出展: Lineaje Report Reveals 82% Of OSS Components Are ‘Inherently Risky’ Due To Security Issues OSSライブラリの脆弱性の情報はJVN iPedia<脆弱性対策情報データベース>や様々なIT系のニュースサイトなどで最新の情報が共有されていますが、サービスで使用している大量のOSSの脆弱性の情報収集を人力で継続的に行うことは至難であり、重要な脆弱性の抜け漏れを生み出しかねません。脆弱性の影響範囲、深刻度によってはサービスをご利用いただいているお客様にも多大なご迷惑をかけることとなり、貴重なお時間、お客様の大切なデータを危険にさらすことになります。 スタンバイの求人検索エンジンが使用しているライブラリに脆弱性が発覚した場合、ニュースで騒がれたことを社員が認識し社内で対応が始まるのではなく、検知システムにより脆弱性が検知され、担当チームが迅速に対応できる仕組みは安心してお客様が使われるサービスの運営に必要不可欠と考えています。 情報収集は受動的に できるようにしておき、 対応が必要になった時は能動的に 動けるようにしておくことが大切と考えています。 最後に 今後もお客様に快適で最適な仕事探しが行える求人検索エンジンの開発、運用に取り組んでまいります! スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに 初めまして、株式会社スタンバイSQG(Search Quality Group)の前川と申します。 SQGのミッションは検索品質の評価で、評価結果をプロダクト開発にフィードバックしています。 また、私は組織の中で別の役割を担っており、その1つにKPIの品質管理があります。この業務の目的は、組織の意思決定を安定的にサポートすることです。 本記事では、この「KPIの品質管理」において工夫した、Redashのデータ出力をより簡便にするための取り組みを紹介いたします。Redashを活用されている方には、特に参考になる内容となっておりますので、ぜひともご一読ください。 各種ツールを簡単に紹介 今回の取り組みで使用したツールはPythonとRedashです。それぞれについて、どのようなツールかを説明します。 Python Pythonはオープンソースのプログラミング言語です。多様なモジュールを用いて、数学計算、日付処理、データベース操作などの作業を行うことができます。 今回はPythonでRedashAPIを実行するモジュールを使い、Redashからデータを取得することとしました。 Redash Redashはデータを可視化し分析するのに役立つBIツールのオープンソースソフトウェアです。Redashで集計したデータはCSV等のフォーマットでダウンロードすることも可能です。 私は普段の業務でRedashを操作して指標を集計していますので、今回の作業についてもRedashを通じて指標を集計しています。 PythonでRedashのデータを抽出しようと思った経緯 困っていたこと 普段の業務においては、Redashからで取得したデータをGoogleスプレッドシートに転記し、数値チェックを行って品質管理をしています。 Redashは非常に便利なツールなのですが、日々のルーチンワークを実行しようとした場合は以下のような作業を手動で行う必要があり、煩雑になります。 Redashを実行して指標を集計する 集計した指標をダウンロードする ダウンロードしたデータを特定のフォーマットに整形する 1度の作業であればそれほど時間もかかりませんが、日々の業務となれば話は変わってきます。「なるべく簡便に集計したい。自動的に欲しいフォーマットに整形することで作業ミス自体も減らしたい。」といった思いで今回自動化に踏み切ることにしました。 今回ご紹介するプログラムを使えば、Redashからjson形式でデータを取得出来るようになります。 準備 これから具体的なコードを交えて実装方法を説明します。 以下のようなテーブルでデータを取得したいので、まずはRedash用のテーブルとSQLの準備をします。 具体例として、以下のデータが保存されているとします。テーブル名は stanby_table とします。 date user_id action 2023-12-01 id_1 view 2023-12-01 id_2 click 2023-12-02 id_1 view 2023-12-02 id_1 click 2023-12-02 id_2 click 2023-12-03 id_1 view 2023-12-03 id_3 view また、Pythonを通じて実行する際には、RedashのURLが必要となります。 今回は仮のURLとして https://redash-host-url/queries/12345 を使って進めます。 こちらのRedashのページに、以下のSQLを保存し、 stanby_table からデータを取得できる状態にします。 SELECT distinct user_id FROM stanby_table WHERE date(date) >= date('{{ start_date }}') AND date(date) <= date('{{ end_date }}') LIMIT 10 ; 上記のSQLは、 stanby_table から、任意の日付を指定して日付範囲のuser_idを重複なく取得します。 後の処理の都合上、日付の指定の変数は start_date と end_date としておきます。 実装の方法 次に、PythonからRedashAPIを利用してデータの集計を行う実装を進めます。 使用するモジュールはオープンソースの redash_dynamic_query です。このモジュールで、RedashのAPIを実行できます。なお、本記事でご紹介する redash_dynamic_query のバージョンは 1.0.4 となります。 Pythonでこのモジュールを実行して、Redashからデータを抽出したいので、まずは、Pythonのスクリプト上で以下のコードを記載し、モジュールをインポートします。 インポート とは、別のファイル(モジュール)に記述されたPythonコードを取り込む機能のことです。 以下のコードを実行することで、 redash_dynamic_query を使用できるようになります。 from redash_dynamic_query import RedashDynamicQuery 次に、RedashDynamicQueryの引数を設定します。コードは以下です。 redash = RedashDynamicQuery( endpoint = 'https://redash-host-url/', apikey = 'your_api_key', ) endpoint はRedashのURLのエンドポイントになります。今回は https://redash-host-url/ です。 apikey はRedashのアカウント編集画面で確認できます。 以下の手順で画面を進めれば確認可能です。 Redashを開く settings の画面を開く Account のタブを開く API Key欄 の API Key を確認する 記載されている API Key をコピーして、上記のプログラムの your_api_key 部分を書き換えます。 次に、クエリの指定を行います。 query_id の変数には、SQL固有の識別番号を代入します。入力する文字はRedashのURLにある queries/ 直後の番号となり、今回は 12345 です。 query_id = 12345 次に、開始日と終了日の日付を指定します。 start_date と end_date のそれぞれに、文字列で日付を記載してください。形式は yyyy-mm-dd となります。 start_date = '2023-12-01' # 開始日 end_date = '2023-12-02' # 終了日 次に、日付の型をRedashに適した形に変更します。こちらはお手元のテーブルの日付の型次第なので、必要に応じて設定してください。 start_date = datetime.strptime(start_date, '%Y-%m-%d').strftime('%Y-%m-%d') 次に、ここまで設定した内容でRedashからデータを取得します。 以下のコードを実行すると、Redashの実行結果を変数 result にjson形式で代入します。 result = redash.query(query_id, {'start_date': start_date, 'end_date': end_date}) これで、Pythonのスクリプトを用いてRedashのデータをjson形式で取得出来るようになりました。 以降は、jsonからデータフレームなど利用用途に合わせて型変換することで、スプレッドシートに簡易に転記したり、グラフ描画での利用、CSVファイル等で出力結果の保存などができます。 ここまでの内容をまとめたコード全体は以下となります。 # モジュールのインポート from redash_dynamic_query import RedashDynamicQuery # endpoint と apikey の指定 redash = RedashDynamicQuery( endpoint = 'https://redash-host-url/', apikey = 'your_api_key', ) # どのSQLを実行するかを指定 query_id = 12345 # 開始日と終了日を指定 start_date = '2023-12-01' # 開始日 end_date = '2023-12-02' # 終了日 # 日付の型を RedashDynamicQuery に適した形に変更 start_date = datetime.strptime(start_date, '%Y-%m-%d').strftime('%Y-%m-%d') # Redashの実行内容を result にjson形式で格納 result = redash.query(query_id, {'start_date': start_date, 'end_date': end_date}) スタンバイの中の活用事例 冒頭で述べたとおり、私は「KPIの品質管理」の業務を行っており、その一環で今回のプログラムを作成することで、従来困っていたRedashの実行から特定のフォーマットへの整形を、今回ご紹介したPythonのコードで完結できるようになりました。 実際の業務では、紹介したPythonのコードに加え、jsonをデータフレームに変更してGoogleスプレッドシートに転記する処理を行っています。これにより、分析を開始するまでのステップを簡略化し、結果分析のための時間を多く取ることができています。 実装した感想 実装する前は、手作業が多くあり、ヒューマンエラーが生じる危険性もありました。 今回の実装を通じて分析のプロセスまでの作業をなるべく簡略化できたので、データの確認や分析にも多くの時間を割けるようになりました。結果、KPIの品質担保に貢献できていると感じています。 今回紹介したプログラムを使って、Redashの実行結果をPythonで取得できるようにしました。これにより、Pythonを使用してデータ分析が可能となり、さらに高度な分析も行えます。 今後は、この方法を施策の分析にも応用しようと考えています。 まとめ 今回の記事では、PythonでRedashのデータを取得する方法について、具体的なコードを交えて紹介しました。 こちらの処理をすることで、手を動かしていた作業の一部を自動化でき、その結果KPIの分析に費やす時間を増やすことができました。 プログラム自体は非常にシンプルで、かつ今後発展性も見込める実装となりましたので、ご活用いただける内容でしたらとても嬉しいです。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
検索エンジンをVespaへ移行しています こんにちは、スタンバイで検索周りの開発を担当している鷹取です。 今回はスタンバイで利用している検索エンジンをVespaへ移行している話を紹介します。 検索エンジン移行の背景 Stanby Tech Blogの スタンバイ2+1年の軌跡 の記事で説明されている通り、 スタンバイでは、主に求人検索機能を提供していますが、その中でもオーガニック(無料掲載)と広告(有料掲載)という2種類の検索が存在します。 この2種類の検索ではそれぞれで異なる検索エンジンを使用しています。 オーガニック検索: Yahoo! ABYSSという検索プラットフォーム 広告検索: Elasticsearch このようになっている背景については、前述の記事に詳細が記載されていますので、興味がある方はそちらをご参照ください。これまで、この2種類の検索エンジンを運用してきましたが、それぞれに課題を抱えていました。 オーガニック検索における課題 オーガニック検索における課題としては、 まず、検索エンジン自体を利用できなくなるリスクです。 ABYSSの提供が終了した場合、スタンバイのサービス全体が停止してしまう可能性があります。 次に、開発上の制約があります。 スタンバイはABYSSという検索プラットフォームの利用者であるため、エンジン自体の開発ができません。その結果、検索エンジンの開発や精度の向上において、自社で完結できず他社に依存しています。 また、スケーリングの点でも問題が生じています。 契約の都合上、クラウドサービスのように即座にサーバ台数を増減できず、 トラフィックに応じた柔軟な変更ができません。 このため、常に最大のトラフィックに耐えられるサーバ台数を確保しておく必要があります。 最後に、経験と知識の面での課題も挙げられます。 他社の検索エンジンを利用していると、 スタンバイ内で検索エンジン開発の知識や運用経験が蓄積されず、 高度な検索エンジンの開発や運用が難しい状態にあります。 広告検索の課題 広告検索の課題としては、独自に開発しているプラグインの開発・メンテナンスの負担が挙げられます。 スタンバイでは、Elasticsearchの独自プラグインを開発し、ランキング機能を実現しています。 このプラグイン導入当時、既存のOSSプラグインでは、実現したい仕様や性能を満たせなかったため、独自で実装することにしました。 しかしながら、ランキング変更のたびにプラグインの改修をしなければならず、改善のボトルネックとなってしまっています。 また、Elasticsearchのバージョンを上げるたびに対応が必要です。加えて、インフラコストの問題もあります。 上記のプラグインの影響により、性能を満たすために必要なサーバの台数が多くインフラコストの増加につながっています。 共通の課題 さらに、オーガニックと広告で異なる検索エンジンを運用していることによる課題も存在しています。 異なるエンジンを利用していることから、精度改善のための実装がそれぞれのエンジンで共有できず、 同じ施策でもオーガニックと広告で別々に実装しなければなりません。これにより、実施できる全体の精度改善施策の数が減少しています。 また、学習コストや運用にかかるコストも2倍になり、本来の検索改善を行うための時間が減少してしまっています。 検索エンジンの統一の決定 これらの課題を解消するため、以下のような目的で検索エンジンを統一し、自社で運用する方針が決定されました。 社内の検索エンジンを統一することで、エンジニアリングリソースを集約し、検索エンジン運用と検索サービス開発の効率を上げる 自社で検索エンジンを運用することで、情報検索技術や検索エンジンに関する深い知識を社内に蓄積する システム提供、ソフトウェアライセンスに関して自社でコントロールできない制約事項を可能な限り排除し、自由度高く検索サービスの開発を行えるようにする 検索エンジンの選定 検索エンジンを統一するにあたって、スタンバイでは最終的に Vespa を採用しました。 ここからは、なぜVespaを選んだかについて説明します。 スタンバイの検索の特徴 検索エンジンの選定に当たって、まずは現状の検索の特徴を把握する必要があります。 スタンバイの検索の特徴をまとめると以下のようになります。 検索 高トラフィック 国内有数の求人検索エンジン サービスの成長に伴い今後も更に増加見込み 低レイテンシー スタンバイは検索が主体なので、検索結果画面の表示に時間がかかると、ユーザが離脱してしまいます。広告も検索なので、売上の低下に直接つながってしまします。 更新 検索対象のドキュメント量が多い 1000万件以上の求人データを扱っています。また、求人検索エンジンの特性上、特定の求人だけ検索できるのではなく、すべての求人が等しく常に検索できる状態になっている必要があります。 ドキュメントの登録と削除が頻発する 求人票は各社の採用状況に応じて、頻繁に公開、非公開が行われます。 新規公開求人の反映速度も重要ですが、特には応募時のトラブル防止のために求人票の取り下げや更新については求人データがスタンバイに連携されたら即時反映させる必要があります。 後述する機械学習に使うための、特徴量データの反映のための部分更新も必要となります。 機械学習 スタンバイでは、検索結果のランキングに機械学習モデルを活用しています。 GBDTモデルを用いたランキング two-phase ranking 機械学習モデルを作成している機械学習チームと検索基盤チームが別 独自のプラグインだと、機械学習チームが検索基盤チームに依存してしまいます それぞれで改善を行えるような体制が望ましいです。 次に検索エンジンVespaの概要と特徴について説明します。 Vespaとは Vespaとは、オープンソースのbig data searving engineです。 オンラインでビッグデータにAIを適用できることが特徴です。 Vespaは検索に限らず、レコメンドや会話AIなど、様々な用途に利用できます。 もともとは、Yahoo(米)の社内で開発されていましたが、 2017年にオープンソース化 1 され、2023年10月にはYahoo(米)からスピンアウトし独立した企業となりました。 2 Yahooの検索での長い実績があり、大規模な量のドキュメントとトラフィックに対応できる能力が証明されています。 1日に25Bのリアルタイムクエリと75Bのライティング(更新)を処理可能です。 また、Spotifyなどのグローバルに大規模なサービスを展開する企業でも採用され始めています。 3 Vespaの特徴 高速な検索と高いスケーラビリティ Vespaはリアルタイムで低レイテンシかつ高スループットが求められるユースケースに最適化されています。 数十ミリ秒以下のレイテンシでレスポンスを返すことが可能です。 また、並列にクエリを実行することで、どのようなクエリ量、データ量でも一定の応答時間を維持できるように設計されています。 さらに、クエリごとに複数のサーチャースレッドを活用し、スループットに対してレイテンシを柔軟にスケールできます。 また、Vespaは高いスケーラビリティも備えています。 Vespaのドキュメントはバケットと呼ばれる単位で管理されます。 バケットのサイズと数はVespaによって完全に管理され、 手動でシャーディングを制御する必要はありません。 そのため、ノードをクラスタに追加するだけで簡単にスケールできます。 将来的にアクセスが増加してもスケールアウトが容易です。 効率的なインデックス作成 Vespaのインデックス作成メカニズムは低レイテンシでの更新に最適化されており、常にデータが変化するシナリオに適しています。 通常の検索エンジンのインデックス作成は、リアルタイムの書き込みを実現するために、 書き込みに対してイミュータブルな転置インデックスのセグメントを構築し、 バックグラウンドでそれらをマージすることで行われます。 大きなセグメントとのマージには非常に長い時間がかかるため、 多くの場合、徐々に大きくなる複数のセグメントを使用し、複数回マージする必要があります。 この方式の場合、高い書き込みレートを維持すると、 クリーンアップしなければならない多くのゴミを作成することになります。 これにより、安定した書き込みレートとクエリレートの維持に問題が生じます。 Vespaも以前はこの方式でしたが、2010年以降は異なる方式を採用しています。 4 Vespaはイミュータブルなインデックスセグメントの前にミュータブルなインメモリインデックスを持ちます。 変更はインデックスセグメントの代わりにメモリ内のB-treeに書き込まれ、 バックグラウンドで不変なインデックスとマージされる設計に変更されました。 この設計ではインデックスセグメントを徐々に大きくしていく必要がなく、 ガベージコレクションや大きなメモリ操作の発生が非常に少ないというメリットがあります。 また、更新リクエストが完了した時点で、そのドキュメントは検索可能になります。 豊富な機械学習関連の機能 VespaはそもそもビッグデータセットにAIを適用するためのプラットフォームとして作られているため、 機械学習関連の機能が豊富にあります。 例えば、以下のような機能を持ちます。 ONNX,XGBoost,LightGBMといった複数のモデルサポート weightedsetやtensorなどのデータ型が使用可能 multi phase rankingのサポート Vespaではrank-profileとよばれる形式でランキングを設定します。 rank-profileでは、様々なランク式や特徴量を組み合わせてランキングのアルゴリズムを定義できます。 また、rank-profileは設定ファイルとしてVespaクラスタに直接デプロイします。 これにより、ランキングアルゴリズムを検索クエリとは分けて管理できます。 運用面でも、Vespaは本体に組み込みで機械学習の機能を持っているため、 プラグイン等を管理する手間がありません。 スタンバイの検索とVespaの相性 スタンバイの検索の特徴とVespaの特徴を以下にまとめました。 スタンバイの検索 Vespa 検索 低レイテンシ 高トラフィック 高速な検索 高いスケーラビリティ 更新 検索対象のドキュメントが多い ドキュメントの更新量が多く高頻度 効率的なインデックスの作成 機械学習 機械学習を活用 機械学習チームと検索基盤チームが別 豊富な機械学習関連の機能 rank-profileによるランキングアルゴリズムの指定 このように、上記で述べたスタンバイの検索の特徴とVespaの特徴がマッチしていたため、Vespaを採用しました。 次章から、具体的にVespaに移行した方法を紹介します。 Vespaへの移行 Vespaへの移行は、以下のようなステップで進めていきました。 Vespaの調査・機能検証 Vespaクラスタの構築 機能開発 テスト Vespaの調査・機能検証 現在提供している検索仕様をVespaで全て満たせるかどうかの調査を実施しました。 実際の移行可能性を確認するために、公式ドキュメントを読みこみ、その後、ローカル環境でクエリを作成して検証しました。 Vespaは公式ドキュメントが充実しているため、 ドキュメントを読めば、どのような機能があるか、どのように使えばいいかがわかります。ただし、ドキュメント量は多いので、全ては読み切れておらず、少しづつ読み進めています。また、VespaのSlackでは開発チームに直接質問を投げることができます。ここで、過去の質問を検索することでも、参考になります。 VespaはDockerイメージも公式で提供されており、ローカルでの検証も簡単にできました。 ここからは、調査したVespaの機能をかいつまんで紹介します。 詳細は、 Vespaの公式ドキュメント を参照してください。 Vespaの紹介 - Vespaのアーキテクチャ 最初に、Vespaのアーキテクチャを説明します。 Vespaのアーキテクチャは以下の図のようになっています。 画像出典: Vespa Overview( https://docs.vespa.ai/en/overview.html ) 図のように、Vespaは複数のコンポーネントから構成されています。 コンポーネント 説明 Admin/Config 設定の管理、クラスタの制御など Stateless Java Conatiner 入力データやクエリ・レスポンスを加工するステートレスなコンポーネント。クエリとデータの操作をコンテンツクラスタの適切なノードに渡す。Javaで実装されており、プラグインで容易に拡張できる 。 Content インデックスの管理を担当する。検索・ランキングはここで実行される。C++で実装されている Vespaクラスタに属する各サーバは、これらのいずれかの役割を担い協調して動作します。 Vespaの紹介 - クラスタの設定 先ほど、アーキテクチャについて紹介しました。 Vespaでは設定ファイルで、クラスタの構成を管理します。 ここでは、Vespaのクラスタ設定について説明します。 クラスタ設定は、以下の2種類のファイルで行います。 hosts.xml services.xml hosts.xml hosts.xmlはクラスタに参加するサーバを定義します。 ホスト名に対して、設定の中で使うエイリアスを指定します。 以下が、hosts.xmlの例です。 <? xml version = "1.0" encoding = "utf-8" ?> <hosts> <host name = "node0.vespanet" > <alias> node0 </alias> </host> <host name = "node1.vespanet" > <alias> node1 </alias> </host> ... </hosts> services.xml services.xmlはVespaクラスタの構成を定義します。 hosts.xmlで定義された各サーバに役割を与えます。 また、redundancyといった冗長化の設定やプラグインの設定およびスレッド数・メモリ等のリソース設定もこちらで行えます。 以下がservices.xmlの例です。 <? xml version = "1.0" encoding = "utf-8" ?> <services version = "1.0" xmlns : deploy = "vespa" xmlns : preprocess = "properties" > <admin version = "2.0" > <configservers> <configserver hostalias = "node0" /> <configserver hostalias = "node1" /> <configserver hostalias = "node2" /> </configservers> <cluster-controllers> <cluster-controller hostalias = "node0" jvm-options = "-Xms32M -Xmx64M" /> <cluster-controller hostalias = "node1" jvm-options = "-Xms32M -Xmx64M" /> <cluster-controller hostalias = "node2" jvm-options = "-Xms32M -Xmx64M" /> </cluster-controllers> <slobroks> <slobrok hostalias = "node0" /> <slobrok hostalias = "node1" /> <slobrok hostalias = "node2" /> </slobroks> <adminserver hostalias = "node3" /> </admin> <container id = "feed" version = "1.0" > <document-api/> <document-processing/> <nodes> <node hostalias = "node4" /> <node hostalias = "node5" /> </nodes> </container> <container id = "query" version = "1.0" > <search/> <nodes> <node hostalias = "node6" /> <node hostalias = "node7" /> </nodes> </container> <content id = "news" version = "1.0" > <min-redundancy> 2 </min-redundancy> <documents> <document type = "news" mode = "index" /> <document-processing cluster = "feed" /> </documents> <nodes> <node hostalias = "node8" distribution-key = "0" /> <node hostalias = "node9" distribution-key = "1" /> </nodes> </content> </services> Vespaの紹介 - ドキュメントの管理 つぎに、Vespaのドキュメント管理について紹介します。 Vespaでは、ドキュメントのスキーマを、 .sd という拡張子のスキーマ定義ファイルで管理します。 スキーマ定義ファイルの例を以下に示します。 schema news { document news { field news_id type string { indexing: summary | attribute attribute: fast-search } field title type string { indexing: index | summary index: enable-bm25 } field abstract type string { indexing: index | summary index: enable-bm25 } field url type string { indexing: index | summary } field date type int { indexing: summary | attribute attribute: fast-search } field clicks type int { indexing: summary | attribute } field tensorfield type tensor<float>(x{},y{}) { indexing: attribute | summary } } fieldset default { fields: title, abstract } } field キーワードに続けてフィールド名と型を指定します。 intやstringといった基本的な型のほか、tensorやweightedsetといった複雑な型や構造体の定義も可能です。 また、フィールドごとにインデクシングの方法や検索方法を指定できます。 例えば、 indexing パラメータを指定することで、インデックス作成時にフィールドのデータをどのように処理するかを設定します。 indexing には以下の3つの値を指定できます。また、複数組み合わせての指定も可能です。 index テキストマッチ用のインデックスを作成します。形態素解析が行われます。 attribute メモリに保持します。ソートやグルーピングに使用可能です。また、完全一致、プレフィックス一致、大文字小文字を区別する一致などが可能です。 summary 検索結果のレスポンスに含まれるドキュメントの情報(summary)に指定されたフィールドを含めます。 また、 index パラメータや attribute パラメータを指定することで、検索の高速化なども可能です。他にも、 fieldset を使うことで、検索用にフィールドをグループ化できます。 注意点として、Vespaでは動的なフィールドは作成できません。 フィールドを追加する場合は明示的に指定する必要があります。 Vespaへのドキュメントの登録方法ですが、 /document/v1/ APIにHTTPリクエストを送る、 もしくは、Java製のfeed-clientでフィードを行うことができます。 /document/v1/ APIは検索用のAPIと別のAPIで、 IDを指定したドキュメントの取得や、登録・更新・削除が可能です。 Vespaの紹介 - 検索の方法 続いて、Vespaの検索方法について紹介します。 Vespaの検索クエリはYQLというSQLに似たDSLで記述します。 select * from news where title contains "vespa" select でレスポンスに含めるフィールドを指定します。スキーマ定義で summary を指定したフィールドを選択できます。 from では検索対象のドキュメントタイプを指定します。 そして where でさまざまな検索条件を指定します。上記の例では、titleフィールドに"vespa"を含むドキュメントを検索しています。他にも、フレーズ検索や緯度経度による検索など、様々検索が可能であり、 Apache SolrやElasticsearchで提供されている基本的な検索機能は一通り揃っています。 また、ランキング後に検索結果をグルーピングする機能も提供されており、集計やdedupe 5 なども可能です。 検索リクエストをVespaに送る際には、 yql だけでなく、タイムアウト時間やヒット件数など検索に関する他のパラメータも同時に指定可能です。 特に使用するrank-profileの名前を指定することで、検索結果のランキングをクエリごとに変更できる機能は、オンラインABテスト時などに便利です。 { " hits ": 200 , " model ": { " locale ": " ja " } , " timeout ": " 1s ", " offset ": 0 , " ranking ": { " profile ": " vespa-test " } , " yql ": " select * from news where default contains \" vespa \" " } また、rank-profileとは別のquery-profileとよばれる機能を使うことで、検索パラメータのセットを名前をつけて管理できます。 これにより、検索時にquery-profile名だけ指定すれば良く、 毎回多数のパラメータを付けてリクエストせずにすみます。 query-profileは以下のように設定します。 <query-profile id = "MyProfile" > <field name = "hits" > 20 </field> <field name = "maxHits" > 2000 </field> </query-profile> Vespaの紹介 - ランキングの指定 最後にVespaのランキングの指定方法について紹介します。 本記事で何度も出てきていますが、Vespaのランキングはrank-profileで設定します。 rank-profileは以下のように設定します。 rank-profile my-rank-profile inherits base { function myfeature() { expression: fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness, 2) } first-phase { expression { attribute(quality) * freshness(timestamp) } } second-phase { expression: lightgbm("test_model.json") rerank-count: 50 } } rank-profile キーワードのあとに、rank-profileの名前を指定します。 inherits を使うことで、他のrank-profileを継承できます。 これは、ABテストなどで、ランキングの一部を変更したい場合(second-phaseのみ変更するなど)に非常に便利です。 function でランキング式の一部として、また、特徴量として使用可能な独自の関数を定義できます。 さらに、Vespaでは多段階ランキングでランキングの負荷と精度のバランスをとることが最初から可能です。 first-phase で負荷が軽いランキング式を指定し、 second-phase はfirst-phaseでランキングされた上位の結果に対して、 負荷が重いがより精度の高いランキング式を指定します。 Vespaクラスタの構築 ここまで、Vespaの機能の一部を紹介しました。 ここからは、実際にスタンバイでVespaクラスタを構築した方法を紹介します。 クラスタの構成 以下が現在のVespaクラスタの構成です。 スタンバイではAWSを使用しています。 そのため、各サーバはEC2を使用してVespaクラスタを構築しています。 high-availability(HA)のため、multi-node構成をとっており、 admin/configクラスタは3台のノードから構成されています。 containerクラスタは、フィードを処理するためのクラスタと検索クエリを処理するためクラスタを分けています。 これにより、フィード、クエリそれぞれで、負荷に応じてノードの性能・台数を 変更できるようにするとともに、フィードの負荷が検索へ影響しないようにしています。 containerクラスタはステートレスであるため、簡単にスケールアウトが可能です。 各クラスタごとのインスタンスタイプは負荷に応じて異なるものを使用しています。 contentクラスタのノードはデータを保持し検索を処理するため、他のクラスタと比べて大きなインスタンスタイプを使用しています。 また、ディスクへのアクセスを多く行うため、インスタンスストアを持つインスタンスタイプを選択しています。 クラスタの構築方法 つぎに、スタンバイでのVespaのクラスタ構築方法を説明します。 検証中何度もVespaのクラスタを構築し直す必要があったため、インフラをコード化しています。 まず、Vespaがインストールされたマシンイメージ(AMI)の作成します。 AWSのEC2 Image Builderを使用して、ゴールデンイメージを作成します。 この際、VespaだけでなくDatadogAgentなど監視や運用に必要なツールもインストールしています。このように、Vespaをインストール済みのAMIを作成しておくことで、 インスタンス起動時間を短縮できます。また、 検証環境で確認したものと全く同じ状態のノードを本番に構築でき、 以前のバージョンの環境にもどすことも容易になります。 つぎに、Vespaクラスタ用のインスタンスを起動します。 台数やインスタンスタイプ、ネットワーク構成などの設定は、すべてTerraformでコード管理しています。このため、新しいVespaクラスタを構築する際は、既存の設定をコピーし、パラメータを変更して terraform apply コマンドを実行するだけで簡単に構築できるようになっています。この時点では、Vespaクラスタの設定はまだ反映されていなため、各ノード上でVespaのプロセスは起動していますが、クラスタとして協調して動作できません。どのノードがどの役割を持つかも決まっておりません。それらの設定は前述したhosts.xmlやservices.xmlで行います。 ただし、config serverだけは、 VESPA_CONFIGSERVERS という環境変数をすべてのノードに設定して置く必要があります。これは、Vespaの設定ファイルを管理するconfig serverのホスト名を指定するための環境変数です。この環境変数を設定することで、各ノードはconfig serverに接続し、設定ファイルを取得します。 最後に、Vespaの設定ファイルを反映させます。 Vespaの設定ファイルは、アプリケーションパッケージという単位にまとめられデプロイされます。アプリケーションパッケージにはデプロイと実行に必要なすべての設定、コンポーネント、機械学習モデルが含まれています。 スタンバイでは、Jenkinsを使用してGithub上で管理しているリソースからアプリケーションパッケージを作成し、Vespaにデプロイしています。 アプリケーションパッケージのデプロイが完了すると、各ノードがVespaクラスタとして協調して動作を開始します。 機能開発 次に、検索エンジン移行のために必要だった機能開発について説明します。 以下が、今回実施した開発の一覧です。 クエリ処理を一箇所に集約 オーガニックAPI・広告API・バックエンドAPIで、クエリを変換するロジックが別々に実装されていたため、エンジンを統合する前にクエリ処理を行うAPIを作成し統一しました。 検索APIの実装 ユーザから受け取ったクエリをYQLに変換 rustでAPIを実装 Feederの実装 ドキュメント1件毎にVespaへの更新リクエストを発行 JavaでStream処理を実装 Linguisticモジュールの実装 形態素解析などの言語処理機能を実装 機械学習モデルの実装 既存のモデルで使用している特徴量を、Vespaで使用できる特徴量にマッピング rank-profileの作成 ここでは、特にLinguisticsモジュールの開発について詳しく説明します。 Linguistics VespaはLinguistics(言語処理)モジュールを使用して、 インデックス作成および検索時にドキュメントやクエリのテキストを処理します。 Linguisticsモジュールは、以下の処理を実装しています。 言語特定 tokenizing normalizing(アクセント記号の除去) stemming これらの処理がLignusiticsモジュールで行われた結果のtermがインデックスに追加されます。 また、検索時にも同様に、クエリのテキストに対して処理が行われます。 Lignusiticsモジュールは、containerクラスタで動作するため、Javaで実装されています。 Vespa公式でもいくつかの実装が同梱されていますが、カスタマイズしたい場合には、 com.yahoo.language.Linguistics インタフェースを実装します。 今回は、自分たちでカスタマイズできるように、Linguisticsモジュールも自前実装したものを使用しています。 実装にあたっては、以下のような実装を参考にしました。 SimpleLinguistics 英語のステミングのみ提供 LuceneLinguistics LuceneのAnalyzerを使用した実装 OpenNlpLinguistics OpenNLPを使用した実装 KuromojiLinguistics LINEヤフー株式会社が公開しているKuromojiを使用した実装 特にKuromojiLinguisticsは日本語の形態素解析用の実装のため、非常に参考になりました。 Linguisticsの注意点として、テキストの処理時に言語を特定しますが、 その特定が間違っているとマッチしなくなってしまいう点があります。 特に、クエリに含まれるワードなど、短い単語は言語の特定が困難なため、 検索パラメータ等で言語を指定したほうが良いです。 また、当然ですが検索とフィードで同じLingusitcsモジュールの実装をすることも重要です。 テスト 最後に、テストについても触れておきます。 テストは、一般的な検索改善と同じように、以下の種類のテストを実施しました。 QA 既存の検索機能が実現できているかの確認 負荷試験 負荷に耐えられるかの確認 選定時にある程度の性能評価は行っていたが、モデル等を本番で使用するものを用意し改めて確認 オフラインテスト 定量・定性の両方で評価 SQ(サーチクオリティ)グループという検索精度を定性的に評価するチームが存在します。 オンラインテスト ABテストを実施 検索精度を定量的に評価 特別なことはしていませんが、検索エンジンの移行ということで、入念にテストを行いました。 移行結果 上記のステップで移行を進めた結果、無事に移行できました。 現状はオーガニックのみ移行完了しており、広告は移行中です。 移行中ではあるものの、検索エンジンを統一し、自社運用するメリットが徐々に出てきています。 まず、オーガニックと広告で同じ検索エンジンを使用することで、検索に関するリソースを集約できそうです。 現段階でも、広告の検索エンジンをVespaへ移行するために、オーガニックの知見を活用できています。 また、自社運用することで、Vespaに関することが中心にはなりますが、情報検索技術や知識を蓄積し始めることができています。 難しかったポイント Vespaの移行に際し、難しかったポイントを紹介します。 まず、覚えなければならないことが多いという点です。 いままで使用していたABYSSやElasticserachとは、 大きく異なる検索エンジンであるため、仕組みを理解するのに時間がかかりました。 単に検索を行うだけであれば、クエリの書き方を覚えるだけで済みますが、 実際に運用をしていくためにはVespaの様々な概念を覚えなければなりませんでした。 Vespaの各コンポーネント上では、proton, config-proxy, config-sentinel, config server, slobrok(Service Location Broker), cluster controllerといった、複数のサービスが動作しています。 運用時、特に何かしらのトラブルが発生した場合の原因を特定する場合、 それぞれのサービスの機能を理解しておく必要があります。 また、日本語の情報が少ないという点もあります。 日本のユーザが少ないため、日本語の知見があまりネット上にありません。 特に言語処理周りは、言語に依存する部分が多く、 自前で実装するにあたっては、コードを読み込んで試行錯誤する必要がありました。 今後について Vespaへの移行は一部完了しましたが、移行しただけで活用できているとはいえません。 今後は、さらにVespaを活用していく予定です。 まず、検索精度改善についてですが、形態素解析の改善、ランキングモデルのさらなる改善、 ベクトル検索(ANN)の導入などが考えられます。 とくに、Vespaは通常の検索とベクトル検索を組み合わせたハイブリッド検索が可能であるため、 現在の検索仕様を維持しつつ、ベクトル検索を導入することができると考えています。 また、オートスケーリングの実装も検討しています。 スタンバイでは時間帯によって検索ボリュームが大きく変わるため、 コストを減らすためにインスタンス台数を最適化したいです。 VespaCloudでは提供されていますが、self-hostingの場合は機能が提供されていないので、 オートスケーリング機能を自前で実装する必要があります。 まとめ スタンバイでは検索エンジンをVespaに移行しています。 複数の検索エンジンを使用していましたが、検索エンジンを統一し、自社で運用する方針を決定しました。 統一後の検索エンジンとして、スタンバイの検索の特徴と合致したVespaを採用しました。 今後は、ベクトル検索の導入など、さらなる活用を進めていく予定です。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com https://blog.vespa.ai/open-sourcing-vespa-yahoos-big-data-processing/ ↩ https://blog.vespa.ai/vespa-is-becoming-its-own-company/ ↩ https://engineering.atspotify.com/2022/03/introducing-natural-language-search-for-podcast-episodes/ ↩ Search and Sushi;Freshness counts ↩ Vespaで検索結果のdedupeを行う方法 ↩
アバター
はじめに 初めまして、株式会社スタンバイのSEOチームの本田です。 スタンバイではElastiCache for Redis (以後 Redis と記載) の バージョン3を長く利用していましたが、 2023年7月31日にバージョン3がEOLを迎えるため、バージョン7へのアップグレードを5月に行いました。 実施したのは半年ほど前ですが、アップグレードに伴い必要だった手順などをご紹介していきます。 期限までにアップグレードしないとどうなるのか AWSからの案内によると、バージョン3は新規作成ができなくなり、稼働中のものは自動的に6.2以上にアップグレードされてしまうようです。(下記一部メールから抜粋) 2022 年 7 月 31 日から 2023 年 7 月 31 日まで - ElastiCache for Redis バージョン 3 インスタンスの Redis 6.2 以降へのアップグレードはいつでも開始できます [3]。 2023 年 5 月 1 日以降 - AWS コンソールから ElastiCache for Redis バージョン 3.x.x インスタンスを作成することはできません。 2023 年 7 月 31 日以降 - ElastiCache for Redis バージョン 3 は ElastiCache コンソール、CLI、API、または CloudFormation では利用できません。2023 年 7 月 31 日以降のメンテナンス期間内に、すべての Redis 用 ElastiCache 3.x.x クラスターを Redis 6.2 に自動的にアップグレードします。 また、 AWS公式ドキュメント によると、5.0.5以前のバージョンからそのままメジャーバージョンのアップグレードを行うとフェールオーバー時のDNS伝搬で最大1分は接続断されてしまいます。 ElastiCache クラスターは 5.0.5 より前のバージョンでアップグレードできます。アップグレードプロセスは同じですが、DNS 伝達中のフェールオーバー時間が長くなる可能性があります (30 秒~1 分)。 今回アップグレード対象となるRedisはほぼ全てのスタンバイのページ表示の際に利用されているので、1分間の瞬断でもあってもユーザーに大きく影響を与えてしまう可能性がありました。 そのため、 Redisをダウンタイムなしでアップグレードする を、対応方針としました。 アップグレードと切り替え方法 ダウンタイムなしで切り替える場合どのようにするかという話ですが、アップデート対象となるElastiCacheのエンドポイントを Amazon Route 53 で以下のようにCNAME設定しており、 redis-service.stanby(例) -> redis.xxxx.ng.0001.xxxx.cache.amazonaws.com 社内の各アプリケーションはCNAMEのホスト名を利用して該当のElastiCacheにアクセスをしています。 そのため、新規でバージョン7のクラスタを構築しCNAME先のエンドポイントを切り替えれば、各アプリケーションのソースコードを変更することなくバージョンアップされたRedisを参照するように切り替えることができます。 また、ElastiCacheはスナップショットから復元する際にエンジンバージョンをアップグレードしてクラスタを作成できます。 稼働中のElastiCacheのスナップショットを取り、そのスナップショットを用いてバージョン7のクラスタを構築することで、データ移行のための設定をすることなくバージョンのアップグレードを行えます。 幸いにもアップグレード対象のRedisに関しては、読み込みはスタンバイのページが表示される度に行いますが、書き込みは非同期になっており一時的に停止可能であるため、Redisのアップグレード作業中は書き込みを停止して新しいクラスタを構築できました。 以上からアップグレードと切り替え方法は下記のようなフローになりました。 手順が確認できたので、次は実際に行ったことをまとめていきます。 アップグレードするために行ったこと アップグレードするために行った手順は下記になります。 Redisのアップグレード内容の確認 バージョンアップしたRedisの立ち上げ 負荷試験 リリース手順書を書いてリハーサル 本番反映 Redisのアップグレード内容の確認 今回はRedisのメジャーバージョンが3から7へと一気に4つも上がるのでアップグレードの内容をよく確認する必要がありました。 主に確認したのは大きく2つ、 パラメータグループ コマンド パラメータグループに関しては地道に AWS公式ドキュメント を読んでいきました。 今回のRedisのアップグレードにおいてはアプリケーションの変更は伴わないため、追加されたパラメータの確認よりも、変更、削除されたパラメータについて重点的に確認をしました。 今回アップグレード対象のRedisはクラスター設定をしておらず、かつ利用しているコマンドも GET , SET , DEL しかないシンプルなものであったため、パラメータグループの変更に影響されるものはありませんでした。 もしクラスタ設定されているのであれば、Redis6での変更点で cluster-allow-reads-when-down : プライマリが落ちたときにレプリケーションの読み込みを許可するか(デフォルトは許可しない) は一度確認したほうが良いと考えられます。 他にもRedis4の変更点でメモリが溢れたときに取る挙動のパラメータ maxmemory-policy に選択肢が増えていたりと、パラメータの変更を見つつアプリケーションのあるべき姿と照らし合わせる必要があります。 次にコマンドですが、これはRedisコマンドの公式ドキュメントと、アプリケーションで用いているコマンドを照らし合わせる必要があります。例えば、 SETコマンド はページの下部にHistoryがあり、取りうるオプションが増えているなどの変更点があります。 Terraformでスナップショットからバージョンあげて復元 Terraformを用いてスナップショットからバージョンを上げて新規にクラスタを作成する方法ですが resourceのクラスタの設定で snapshot_name 属性があるので、そこに引数としてセットしてapplyするだけで新規に作られます。 resource " aws_elasticache_parameter_group " " default " { name = " cache-params " family = " redis7 " } # クラスタ設定はエンジンバージョンに最新バージョンを指定 # snapshot_nameに復元するスナップショット名を指定 resource " aws_elasticache_cluster " " example " { replication_group_id = " cluster-example " num_cache_clusters = 2 node_type = " cache.r6g.4xlarge " port = 6379 engine = " redis " engine_version = " 7.0 " parameter_group_name = " default.redis7 " snapshot_name = " snapshot_name " .... other } 設定中にハマった点は、Terraformプロバイダーのバージョンが古く Error: engine_version: Redis versions must match <major>.x when using version 6 or higher, or <major>.<minor>.<bug-fix> apply時に上記のエラーが出ましたが、 v5.3.0 の PR にて解消されるので、上記のエラーに遭遇したらTerraformプロバイダーのバージョンを上げましょう。 負荷試験 アップグレード対象のRedisは求人検索結果の表示など非常に重要な部分で使われているため、本番反映前に負荷試験を行いパフォーマンス低下や不具合が起きないか確認する必要がありました。 スタンバイでは求人検索機能に関する SLO を設けているため、それを基準に負荷試験を実施しました。 リリース手順書を書いてリハーサル 本番作業をスムーズに問題なく実施できるように、検証環境でリハーサルを実施してから本番作業をすることにしました。 リリース手順書を作成し、チーム内レビューを通して検証環境でのリハーサルに挑みました。 実際に検証環境でCNAME切り替えると想定外の事が起きたので、リハーサルをやってよかったと言えます。実はRedisのCNAMEを切り替えただけだとうまく切り替わってくれないという事象が発生しました。 理由はRedisの Client Timeouts はデフォルトで無制限(つまりコネクションを閉じない)だからでした。 By default recent versions of Redis don't close the connection with the client if the client is idle for many seconds: the connection will remain open forever. 解決方法はRedisに接続しているアプリケーションをデプロイし直すことで新たにコネクションを接続しに行くので解決しました。 本番反映 あとは、本番前日にスナップショットからバージョンをあげたRedisを立ち上げて当日に切り替え作業するだけでした。しっかりとリリース手順書を書いたのでスムーズに切り替えも終わり無事アップグレードできました。 よかったこと アップグレードの対応を通して個人的によかったことを列挙します。 Redis5.0.6から Graviton が対応になっており、同スペックで低コスト運用が可能になったので少しコスト削減に貢献できました。 Redis5.0.6以降は、アップグレードに伴うダウンタイムが最小限に抑えられるので、今回のような大掛かりの作業をしなくてもすむ可能性が出てきました。(参考: アップグレードにと関する考慮事項 ) Redis自体は長く使用はしていたものの、どんな機能があるのか、どんな設定ができるのかなど詳細をあまり意識していませんでしたが、今回のアップグレードの変更内容を一通り読むことで曖昧だった部分について理解が深まりました。 最後に 以上、Redis3をRedis7にアップグレードした話でした。 今回のアップグレードでは、ダウンタイムなしで切り替えを行いました。 今回のアップグレード対応は個人的にRedisについてもっと知りたいと思える良い機会になりました。 参考文献 Amazon ElastiCache for Redisのメジャーアップグレード方法をまとめてみた ダウンタイムなしでアップグレードする方法を参考させてもらいました パラメータグループの変更内容 エンジンバージョンの変更内容 Amazon ElastiCacheのTerraform アップグレードにと関する考慮事項 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに こんにちは。スタンバイで求人データ管理に関するバックエンドエンジニアをしている池田です。 スタンバイはWEB上に存在する大量の求人を一括検索できるサービスを提供しており、その求人票のマスタのデータは Amazon Aurora を使って運用しております。 以下の記事で説明をしておりますが、2021年に求人取込直後の求人情報を構造化データとして保存するために Amazon Aurora を採用しました。 スタンバイの求人情報取込の仕組みを作り直した話 〜序章〜 DBエンジンとして Aurora ( MySQL 5.7 ) を利用しております。 ストレージエンジンとして InnoDB を利用しております。 しかし作り直しから時が経ち、求人票の増加や各種機能の追加等によって Aurora のデータ量は想定上の速さで増加していき、それに比例する形でインフラコストも増加し続けていました。 今回反省点とコスト削減のために実施した施策と結果について、紹介します。 増加しているテーブルサイズのグラフ。(1つの線が1つのテーブルのテーブルサイズを示しています) データ量が増加している要因 一部のテーブルのデータ量が大きく増加し続けている理由は、レコード削除が行われたテーブルに対し、ディスク領域の解放ができていなかったためです。 MySQLやMariaDBでは OPTIMIZE TABLE や ALTER TABLE などのコマンドを実行することで、ディスク領域を解放できます。 1 求人票はかなり頻繁に追加や削除が行われるという特徴があるため、求人データを管理する一連の処理では日常的に大量の INSERT, DELETE が実行されます。 そのため求人票を扱うテーブルのレコード数はあまり変わっていないのに、テーブルサイズはどんどんと増加し続けていました。 データの増加による悪影響 またデータ量が増えるとインフラコスト( storage usage 等)が高くなるだけなく、DBからのデータ取得速度が遅くなってしまい、サービスとしてもレスポンスが遅くなるなどの悪影響が出てしまいます。 DBからデータを取得する際のインデックススキャン、テーブルスキャンが遅くなります。 2 データの取得に時間がかかるため、サービスのレスポンスが遅くなります。 データの取得に時間がかかるため、CPU使用率も増加しやすく、リードレプリカを増やす、インスタンスタイプを上げるといった対応が必要になりさらにインフラコストが上昇します。 OPTIMIZE TABLE を定期的に実行するためには では単に OPTIMIZE TABLE を実行すれば良いのかというと、そうではありません。 なぜならば OPTIMIZE TABLE を実行すると、実行時と終了時に該当のテーブルにメタデータロックがかかるからです。 メタデータロック中に、並行してSelectクエリが実行されるとブロックされます。 3 つまり定期的に OPTIMIZE TABLE を実行するためには、Aurora のメンテナンス時間を設ける必要があり、 24時間稼働しているWebサービスでは原則として停止時間は設けられないので、Webサービスは直接Auroraに依存しないようにアーキテクチャを変更する必要があります。 当時スタンバイでは、Webサービス上で求人票を表示する際にはこのAuroraを直接参照するアーキテクチャになっていました。 そのため、定期的に OPTIMIZE TABLE を実行するためには、以下のようにWebサービスからはAuroraに直接依存しないアーキテクチャにする必要があります。 上記はスタンバイのWebサービスへ提供する求人情報をAuroraからではなくDynamoDBを通して提供するアーキテクチャです。 AuroraからDynamoDBへFeederを行うサービスを作り、AuroraとDynamoDB間求人データの同期を行いました。 求人情報APIではDynamoDBの求人データを参照するようにし、Auroraへの依存をなくしました。 アーキテクチャ変更の施策と結果 Auroraに依存しないアーキテクチャにしたところで、Optimizeをテーブルを実行したところ、データ量が減少しました。 4 初回データ削除後のテーブルのサイズ。 定期的なOptimizeの実行後のテーブルのサイズ。 初回Optimize後に定期的にOptimizeを実行することで、未使用領域がshrinkされたりB-TREEインデックスの並びが整理され、結果的にストレージの使用容量が減少しました。 結果として インデックススキャン、テーブルフルスキャンが速くなり、クエリーの実行速度が上がりました。 クエリーの実行速度が向上することで、DBに対してクエリーを実行するアプリケーションの並列度を下げることができるようになり、特にSELECTクエリーの同時実行数が減りました。 並列実行されるクエリー数が減ることでDBのCPU使用率も減り、さらにDBのクラスターの台数を減らすことができました。 最終的にインフラコストが大きく削減されました。 さらにコストを削減するための施策 スタンバイの求人データの更新頻度を見直して1日の取り込み回数を制限した結果、求人の書き込み数自体が約2分の1ほど減りました。 5 この書き込み数を減らす施策によって、Auroraのインフラコストがさらに削減できました。 2023年3月の Aurora:StorageIOUsage と比較して、2023年8月以降には1/3以下に下がりました。 施策と結果について 今回実施した施策をまとめると以下となります。Auroraのインフラコストは施策実施前と比較して約55%削減できました。 更新頻度が高いテーブルを定期的にOptimizeできる状態にする。 書き込みが多い場合は、書き込みを減らす。 不要なトランザクション処理を削除する。トランザクションを使うべきところで使う。 書き込みの速度が遅くなり、かつ削除できないログ(InnoDBのログ)がたまり続けるため。 Auroraでのコスト削減の面で一番効果的な施策は、DBへの書き込み、読み込みを減らすことでした。 Aurora:StorageIOUsage に比例して、コスト全体が 下がっております。 反省点 今回2023年3月と比較して大きくコスト削減ができたのですが、AWS Aurora(MySQL)を採用する際には以下を気にしておくべきであったと痛感しております。 設計時にデータ量を正しく見積もる。 マスターテーブル、トランザクションテーブルでどれくらいのデータ量になるかを見積もる。 レコードのライフサイクルを考える。 レコードの削除を多く実行する場合には、どこかのタイミングで OPTIMIZE TABLE を実行する必要がある。 MySQLまたはMariaDBを採用する場合は、定期的に OPTIMIZE TABLE できるようなアーキテクチャにする。 書き込みが多いアプリケーションの場合は、I/Oレートに比例して料金が上がり続けない料金体系を利用する。 過剰なSLOを定めない。 今回のケースでは関係者と調整したうえで求人データの更新頻度を1日N回に制限することで書き込み数を減らすことができました。SLO自体が要求・要件に対して過剰になっていないかを確認し、適切なレベルにすることは重要でした。 求人データ管理に関するシステムのリアーキテクチャを進める仲間を募集しています 今回の施策で、求人データの管理に利用しているAuroraの費用は以前よりも半分以下に削減できましたが、まだまだ求人データ管理周りのシステムはコストを削減できる余地があります。 現在はよりcost-effectiveなアーキテクチャを目指して、求人取り込みのリアーキテクチャを進めております。 目標としては更に半分以上にインフラコストを下げたいと思っております。 そのため一緒に求人データ管理周りのシステムのリアーキテクトを進める仲間を募集していますので、少しでも興味があればぜひご連絡ください。 データストアをAuroraだけなく、DynamoDBやDocumentDBなどを適切に利用することで、よりコストを削減できるアーキテクチャにします。 毎日約800万求人の取り込みを行い、大規模なデータ量を扱った開発、保守、運用しております。 言語はJava, Goで開発を進めており、チームでGoやストリーム処理の勉強会をしながらみんなで学習しています。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com 私の Amazon RDS for MySQL DB インスタンスが想定よりも多くのストレージを使用しているのはなぜですか? ↩ 実際にOPTIMIZE TABLEの効果を実測したところインデックススキャンでのデータ取得が、OPTIMIZE TABLEの後に数十秒から0.5秒に改善されました。 ↩ AWSのサポートに確認したところ、以下の返答をいただいております。「DDL文の実行にはメタデータロックの取得が必要となる関係上、select文が並行して実行されている場合、ブロックされる動作となります。また、optimize文はInnoDBストレージエンジンの場合、ALTER TABLE ... FORCEという文となり、開始および完了時にごく短時間、メタデータロックを取得する動作となります。」 ↩ レコード数が多いテーブルではOptimizeに数日かかり正常に完了しない問題も発生したため、テーブルを作りかえて、データを移行することで対応しました。 ↩ AWSには Aurora I/O-Optimizedという I/O 集約型アプリケーションの場合に料金の上昇を抑制できる料金体系があります。 MySQL 5.7ではAurora I/O-Optimizedには対応しておらず、MySQL 8系にすることで、Aurora I/O-Optimizedを利用できるようになります。 今回のAuroraで料金をシミュレーションした結果、料金体系を変更せずに書き込みを減らす案の方がインフラコストを下げられたので、 I/O-Optimizedは利用しませんでした。 AWS が Amazon Aurora I/O 最適化をリリース ↩
アバター
こんにちは、スタンバイで検索周りの開発を担当している鷹取です。 今回は検索関連についてではなく、スタンバイの技術負債解消についての取り組みについてご紹介します。 概要 Stanby Tech Blogの スタンバイ2+1年の軌跡 の記事でも少しだけ触れられていますが、 スタンバイのシステムには複数チームでメンテしているstanby-apiと呼ばれるコンポーネントがあります。 stanby-apiはスタンバイのWeb画面を表示するための重要なコンポーネントですが、 歴史が長いこともあり、コードの複雑化や開発体制とのミスマッチにより、プロダクト全体の開発生産性の低下を招いていました。このstanby-apiの複雑化してしまったコードを、適切な粒度でモジュールに分割することにより、問題の解決を図ったというのが、本記事の内容になります。 stanby-apiとは まず最初にstanby-apiについて説明します。 スタンバイはWeb上で求人検索を提供しているサイトですが、 その実現のためには、以下のような様々な機能が必要になります。 求人の検索 広告配信 住所情報の解析 クエリの解析 クエリのサジェスト 応募機能 ABテスト機能等 これらの機能は、スタンバイ内部の複数のコンポーネントがAPIとして提供しています。 stanby-apiはフロントエンドからのリクエストを受け、 バックエンドの各API群を適切な順で呼び出し、 それらのデータを統合してフロントエンドに返却する役割を担っています。 stanby-apiが抱えていた課題 上記で述べたように、stanby-apiはフロントエンドへのレスポンスを返す役割を果たすコンポーネントですが、最初からそのように設計されたわけではありませんでした。初期段階では、現在ほど多くの機能やコンポーネントが存在せず、バックエンドの機能が直接stanby-apiに実装されていました。時間が経過するにつれ、スタンバイの機能が増え、stanby-apiにも多くの実装が追加されました。しかし、これらの変更は全体のアーキテクチャを総合的に検討せずに行われ、将来の展望に基づいて設計されたものではなかったため、必要な機能が段階的に追加され、APIが無秩序に成長してしまいました。 さらに、組織の拡大と体制の変化に伴うチームの分割や統廃合、開発者の異動などもあり現在では、stanby-apiは複数のチームの開発者によって開発される状態になりました。しかしながら、チーム間の責任分担が明確でなかったため、各チームが自由に実装したコードがいたるところで入り混じり、stanby-apiの全体像を把握する開発者の不足に繋がりました。 結果として、stanby-apiの開発においてさまざまな問題が発生しました。必要な情報の把握には多くの時間がかかり、機能の開発に取り組むまでに遅延が生じました。一見するとささいな変更であっても、無関係のテストが失敗する事例も発生しました。何人もの開発者や複数のチームが同一のコードベースに変更を加えるため、コンフリクトが発生し、リリーススケジュールの管理が煩雑となりました。このような理由から、stanby-apiの開発はスタンバイの成長において大きなボトルネックとなっていました。 モジュール分割による解決 この問題を解決するために、stanby-apiに対するリファクタリングプロジェクトを立ち上げました。 このプロジェクトでは、以下の2つをゴールとして設定しました。 絡み合ったコードを整理し、stanby-apiのコードを機能ごとにモジュールへ分割することで、依存関係を分離し、機能間の境界を明確にすること 各モジュールに担当グループを割り当て、作業範囲を限定、責任を明確にすること プロジェクトを進行する際、まず最初にモジュールの分割方針を策定しました。 モジュールを分割する際、最も検討が必要なポイントは、どの単位でモジュールを分けるかです。分割単位が小さすぎると保守性が低下し、大きすぎるとコードの複雑性の増加につながる可能性があります。しかし、モジュールの分割単位については絶対的な正解はありません。このプロジェクトを進める際、各チームと実際のコードを検討しながら、モジュールの分割単位を合意するためにいくつかの方針を設けました。 モジュールの分割単位は、担当チームが単独で開発・運用できる単位とする。 既にAPIとして独立した機能が切り出されている場合、外部API呼び出しを1つのモジュールとして分割する。 これらの方針を定めることで、モジュールの分割に関する議論を具体的に進め、合意を得る際の指針としました。 方針が固まった後、具体的なモジュール分割方法を決めました。 モジュールのインタフェース定義には、Protol Buffersを採用しました。 これまでは、特定の機能でのみ使用される想定のクラスやメソッドが、stanby-apiのどこからでも呼び出し可能になっていたため、本来関係のない箇所で使用されていることがありました。 例えば、求人情報を表すJobクラスに実装されている求人のタイトルを表示を整えるために加工するメソッドが、検索APIを呼び出しているメソッド内で使われているといったことがありました。このような状況では、変更の影響範囲が特定できず、小さな変更に対しても調査に時間がかかってしまいます。ソースコードを単にモジュールに分割しただけでは、この問題は解決できません。今いるエンジニアが注意を払って開発していても、いずれ新たに入ってきたエンジニアが、意図せずにモジュール外からモジュール内部のロジックを呼び出してしまう可能性があります。 そこで、Protocol Buffersを使用して、モジュール間でビジネスロジックが漏れ出ることを防ぐことにしました。Protocol Buffersでスキーマを定義し、ツールを用いてモジュール呼び出しのインプットとアウトプットを表すクラスのコードを生成します。このクラスには、数値や、文字列、配列、オブジェクト型のメソッドを持たないデータのみが含まれます。モジュールの実装側と呼び出し側は、このクラスにだけ依存するようにすることで、ロジックがモジュールの外に漏れ出てしまうことを防ぐことができます。 モジュールの分割の進め方として、モジュールを分割する際には、一括で全てのモジュールを切り出すのではなく、段階的に進めることにしました。最初に影響が比較的小さいモジュールをトライアルで切り出し、モジュールの分割における実績を積むことに重点を置きました。 そこで得た経験を活かしながら、他の開発に対する影響を最小限に抑えつつ、順次モジュールを切り出していきました。 モジュール分割によって得られた効果 上記の方針に従ってプロジェクトを進め、モジュール分割は約半年で完了しました。 モジュール分割により得られた効果として、複数チーム間での開発が非常にやりやすくなりました。 すべてのコードの担当グループが明確になったため、ある機能を追加しようとした際に、どこのチームに相談をすればよいかが一目でわかるようになりました。 また、スキーマ定義の変更箇所が決まってしまえば、モジュール呼び出し側とモジュールの実装側で並行作業ができるようになり、開発期間が短縮されました。 同じコードを触ることがなくなったので、コンフリクトも減少しリリースの管理も容易になりました。おまけに、誰の持ち物かわからず削除できていなかった大量のデッドコードを削除でき、コードがクリーンになりました。 今後について 今回はstanby-apiというコンポーネントのリファクタリングとして、ボトムアップのアプローチでモジュール分割を進行しました。しかしながら、このような課題は1つのコンポーネントに限らず、プロダクト全体で発生する可能性があります。このような問題を未然に防ぐため、全社的なアーキテクチャの確立が必要と考えられます。 近年、マイクロサービスやモジュラモノリスなどのアーキテクチャが広く採用されつつあります。今回の改修において、stanby-apiはモジュラモノリスとマイクロサービスの中間的なアーキテクチャとなりました。将来的に全社的なアーキテクチャがどのようになるかに関わらず、今回の改修によりスムーズな移行が可能となっています。 まとめ 本記事では、stanby-apiのモジュール分割によるリファクタリングについてご紹介しました。 Stanbyの重要なコンポーネントであるstanby-apiは、長い歴史と複雑性から生産性低下の課題を抱えていました。モジュール分割プロジェクトにより、モジュールの明確な分離と管理体制の整備が行われ、開発生産性が向上しました。将来のアーキテクチャ変更にも柔軟に対応可能な状態となりました。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
株式会社スタンバイ QAグループに所属している扇谷です。 本記事では、スタンバイQAのテスト自動化の取り組みを紹介したいと思います。 2023年9月現在、導入後1年半におけるスタンバイのWebのテストで、テストシナリオ数は「100個以上」になっており、実施回数は「9000回以上」になります。 テスト自動化が標準となっていく業界の流れの中で、スタンバイQAの目的と試行錯誤から、今後、テスト自動化の導入を検討するQAの意思決定の一助となれば幸いです。 背景・課題 〜 なぜテスト自動化をするのか スタンバイQAでは、PythonやNode.jsも使用しテストの一部を自動化していますが、本記事ではサードパーティのテストツールの導入についてお話しします。 テスト自動化ツールを導入時には、まずQA費用のコストダウンに注目してしまいがちですが、その前段階として導入によって何を実現したいのか、しっかりと目標を据えることが重要です。 たとえコストダウンが行えなかったとしても、それ以上の恩恵を前提として導入した場合、より幸せなQAライフが待っていると思います。 コストダウンが一番伝わりやすい導入理由になるのを否定するものではありませんが、自動化テストのシナリオ作成やメンテナンスコストを考慮に入れると、コストダウンは必ずしも容易に達成できるものではない可能性があります。 QA予算を抑えることができるようになること 今までQAで実施できていなかった範囲の作業が行えるようになること 図のE2Eテスト部分のコストカットは数字として出しやすいですが、緑色部分はQAチーム外には実感しにくい遅効性の効果だったりしますので、各QA業務のパフォーマンスを数値として出せる(あるいは新しい取り組みを列挙できるよう)準備はしておいた方がいいかもしれません。 内部的なE2Eテストのコストカットを前提としたテスト範囲の拡大を実現し示すことができれば、成長するチームとして組織の中でより存在感のあるQAとなるでしょう。 導入検討 〜 MagicPod を選んだわけ スタンバイのE2Eテストの自動化導入では、開発状況を反映し、大きく以下の点を達成したいと考えていました。 E2Eテストを開発タスクから「QAタスク」に移行する 以前のスタンバイでは、開発スタッフがE2Eテストを担い自動テストを組んでいました。 QAでも最終的にE2Eテストを実施していましたが、開発段階におけるE2Eテストの一部を実装/実施することは開発サイクルを遅延する一因となっていましたので、開発側のテスト内容をMagicPodに組み込むことで不要なストレスが掛かっていたタスクを取り除くことができました。 ノーコードで作成でき、QA内だけで完結できる テスト自動化の持続性の点から、自動テストのシナリオ作成は技術的に難易度が低ければ低いほどいいです。 MagicPod社のサポートは手厚いですが、数時間ほどの操作で通常使用に問題ないほどのレベルで習熟ができることは、導入時のとっかかりとして重要です。 E2Eのテスト自動化は、マウスのポチポチだけで達成できる時代が既に到来しており、あらゆるレベルのQAスタッフがそれなりに使用すること自体ができますし、自社の開発スタッフのサポートがほぼ不要なほど僅少で済むことは、QAが自律的に活動できる組織になるための大きな援助となりました。 コスパが良い 機能が必要十分以上であること、継続して開発されていること、従量課金制ではないこと、他サービスと比較し安価であること、など総合的に判断しベンダーロックされても問題ないと判断できました。 従量課金制の場合、金銭的な計算や自動テストの作成や実施回数など気を使わなければいけない部分が出てきます。 テスト回数への制限がないMagicPodの料金体系は、QAが思う理想的なテスト回数を実現できる為、金銭的な憂いから解き放たれることはMagicPodを採用する大きな理由のひとつとなりました。 Android/iOSアプリ対応 2022年の導入当時から、ネイティブアプリのテスト自動化に対応していることも導入の決め手となりました。 コスト感がマッチしていたこと、他サービスを利用した場合には1.5〜2倍超ほどコストがかかる可能性があったことも後押しとなりました。 これらを持って、数社のサービスを比較しMagicPodの無料体験から導入を決めています。 恩恵 〜 望外に便利だった機能 画像差分比較 MagicPodでは、キャプチャした画像を期待値として、テスト実行時に比較し差分を検出/通知できます。 スタンバイでは、リリースサイクルの兼ね合いからQAを通さずにABテスト等を実施する必要もあった為、本番環境と検証環境で定期的に実行してくれるMagicPodは導入意義が高くありました。 リリース後の不具合を検出できるか否か以前に、画像差分でABテスト部分の差分を教えてくれることで、QAが絡まないリリース内容を開発↔︎QA間での事前/事後の情報共有/確認が不要となりました。 現行のプロダクトを早期に正確に把握できること、かつ本来必要なコミュニケーションを不要なものとして昇華してくれたことは、QA作業の負荷を軽減してくれたと思います。 メールチェック 公式サイトで外部連携のメールチェックの手法が2種紹介されていますが、弊社ではSlackのチャンネル用のメールアドレス宛にメールを送信しています。 普段使いのSlackでメールを簡易に確認できることは、シナリオ作成時においても利便性が良かったです。 日次の検索やクリックを行う スタンバイのサービスでは、クリック履歴や検索履歴から作成されるデータがあるのですが、検証環境では開発/QAスタッフがメインに触っていますので、操作履歴の絶対量が少なく確認したいタイミングでデータがない場合がありました。 前日や当日の下準備が必要となっていたのですが、MagicPodが毎日、決まった時間にアクセスやクリックを頑張るシナリオを作成することでそのような下準備が不要となり、仮に反映されない現象が発生した場合に、どのタイミングから発生したのかを遡りで原因を特定することも容易になりました。 メリット・デメリット 〜 メリットを大きく引き出す スタンバイではMagicPodの自動実行を、本番と検証の両環境で3時間ごとに回しています。 前述の「今までQAで実施できていなかった範囲の作業が行えるようになること」として可能な限り頻度を高めていきたいのと、テスト結果確認におけるタスク量を許容できる範囲に収めるバランスの落とし所として頻度を決めています。 別途、新規実装のテスト環境でも手動実行を行なっており、今まではテスト依頼ごとに広範でのリグレッションテストやE2Eテストは実施できていませんでしたが、MagicPod導入によりテストカバレッジが増加しました。 スタンバイQAとしては、コストカットよりQAが行える作業量の拡大とコストカット分の時間を他作業に当てることを主眼に置いていましたので、当初の目標は早い段階で達成できたかと思います。 なお、デメリットというデメリットはありませんが、導入時のテストシナリオ作成のタスク量は大きい為、繁忙なQAチームのタスク軽減を目指して導入したのにテストシナリオ作成が滞る可能性は起きうるかと思いますし、スタンバイでも導入直後はその状態にありました。 QAチームの置かれている状況を考慮に入れ、取捨を行いテストシナリオ作成を優先することで数ヶ月〜1年後に想像していた未来を手に入れることができると思います。 デメリットよりもメリットを享受できる未来に持っていくまでのロードマップを正しく描き、正しく実践することが重要かと思います。 反省 〜 導入に向けて気を付けること スタンバイQAのMagicPod導入時に試行錯誤の上、手戻りが発生した点は以下となります。 端的には、誤った前提から始まるテスト作成は、メンテナンス性の悪さから無用の長物と化してしまう可能性が高いです。 E2E・リグレッションのテスト項目は作り直すべき 通常、テスト項目数は機能追加と共に右肩上がりとなり取捨選択が必要となっていますが、テスト自動化の導入により基本的に整理する必要性は低下していきます。 MagicPodのシナリオはテスト手順が多くなりますので、どのようなテストを行なっているかの確認は時間を要しますので、別管理がおすすめです。 また、手作業でテスト項目を埋める際には順不同でも確認できれば問題ないですが、自動化したチェック順と実際のテスト項目の順番に齟齬が発生していると実装内容が追加/改修された時にメンテナンスが煩雑になります。 弊社の導入時を省みても、作業の流れを意識したテスト項目の完成度の高さがスムーズなテストシナリオ作成と以後のメンテナンスコストの軽減に繋がることを、当たり前のことながら強くお伝えしたいと思います。 テストシナリオは小分けにすべき 長すぎるテストシナリオは、テスト結果の確認時にどの機能でアラートされているか判断するのが手間になります。 小分けしすぎるのも問題ですが、冗長なテストシナリオは確認やメンテナンスも煩雑になるので、機能ごとに区切って適切なボリュームに収めて作成することが重要となります。 これらを実践することで、E2E・リグレッションテストの精度も向上し、MagicPodのメンテナンス性も高まります。 導入を機会に、可能であれば項目を見直し刷新すると良いでしょう。 総括 〜 MagicPod MagicPodでは、UI上の操作やHTMLの確認が可能でありますが、Cookieやネットワーク情報の整合性確認は行うことはできませんので、その確認は別途自動化する必要があります。 ■metaタグの確認はデータパターンを用いて各ページで確認 その自動化に需要があるかは置いておいて、どのようなチェックをどのようにテスト自動化するか否かを棲み分けする必要性がありますが、MagicPodの確認範囲は広範に及びますし、そのポテンシャルを最大限に引き出すことでQAの作業タスクの限界値を大きく引き上げてくれるサービスだと実感しています。 長年に渡り継続するプロダクトのQAでは、テスト自動化の導入は必須になっていくと思いますが、QAのタスクを如何に軽減していくことができるか、あるいはカットしたタスク以上のアウトプットを如何に出すことができるかが導入に失敗しないための要となります。 以上が、簡易ながらスタンバイQAのMagicPod導入の歴史となります。 長文でありながら書き足りない部分もありますが、お付き合い頂きありがとうございました。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに こんにちは、コーポレートITグループの西本です。 コーポレートITグループでは、社員のPCの管理や、各種ライセンス管理をはじめ、社内で利用するサービスの導入やサービス間の連携など幅広く業務をおこなっています。 その中で今回は、タイトルにもあるように、kickflowというワークフローシステムとGoogleスプレッドシートを GoogleAppsScript で連携させた話をご紹介いたします。 背景 スタンバイでは、購入稟議や支払い申請を管理するためのワークフローシステムとしてkickflowというサービスを利用しています。 https://kickflow.com/ kickflowで作成された各種リクエストはチケットという単位で管理され、ベルトコンベアの上を流れる出来立てのパンのように、各部署のチェックをもらいながら完了へと進んでいき、すべてのチェックの完了後、リクエストに応じて各部署で物品の購入や、支払い処理などに進んでいきます。 それぞれのチケットは、kickflowのサイト上で1つずつ確認ができますが、業務を効率的に進める上で、完了したチケットを一覧にしてGoogleスプレッドシートへの書き出しをしてほしいという要望があり、以前までZapierというノーコードで複数のサービスを連携させることのできるサービスを利用し、kickflowで完了したチケットのデータをスプレッドシートに書き込んでいました。 Zapier | Automation that moves you forward 何が課題だったのか Zapierはプログラミングに関しての知識がなくても各種サービスを連携させることができる便利なサービスなのですが、この連携についてはいくつかの問題点がありました。 書き込み時にエラーが起こった場合、対象のチケットデータの再取得に手間がかかる。 Zapierは基本的に指定したイベントをトリガーとして処理が開始します。kickflowでチケットが承認されると、そのタイミングでwebhookを送信するよう設定し、それをトリガーとしてZapierでの処理を動作させていました。しかし、webhookを受け取って処理を実行する際にエラーが発生した場合、対象のチケットの再取得に手間がかかるという問題がありました。このため、特定のチケットに対して任意のタイミングでデータ取得やデータ加工が行えるようにする必要がありました。 データの加工の問題 Zapierは基本的にノーコードプラットフォームであり、さまざまなサービスの連携を強力にサポートします。しかし、kickflowで生成されるJSON形式のデータをスプレッドシートへの書き込み用に加工する際には、多くのステップを踏む必要がありました。また、スプレッドシートのフォーマットが変更されると、そのメンテナンスに多くの時間がかかっていました。このため、変化するニーズに柔軟に対応できる連携方法に変更する必要がありました。 使用制限の超過( 昔話です。現在はアップデートにより解決しています ) 現在のkickflowではwebhookを送信するイベントを任意で選べるようになりましたが、以前は全てのイベント(全てのワークフローで作成された全てのチケットのステータス変更が対象)でwebhookが送信されおり、結果として大量の処理がZapier上で行われることになりました。これにより、契約していた月間使用可能な処理回数を超えてしまい、予期せぬタイミングで処理が停止する問題が発生しました。 このような課題を解決するためにZapierでの連携を見直す必要がありました。 GoogleAppsScriptで連携をさせるメリット このような課題を解決するためにコードで細かく処理を行えるGoogleAppsScript(以降GASと表記)で連携させることにしました。 GASを使用するメリットとしては以下のようなものがあります。 GoogleWorkSpaceを契約している場合無償で利用できる ←超重要 独立した実行環境を必要としないため、非専門家にとって業務で使用する敷居が低い GASはJavaScriptがベースになっているため、私にとってコードの記述が難しくない(過去に少しだけ触ったことがありました) このようにGASには多くのメリットがありますが、実装を進める中で同時に解消しなければいけない課題もいくつか見えてきました。 GASで連携を進める中で見えてきた課題 そんなこんなで、GASを用いてkickflowとスプレッドシートの連携を進めていったのですが、いくつか問題がありました。 データをどのように取得するか kickflowのwebhookはタイムアウトが10秒に設定されており、その時間内にデータの加工、添付ファイルのダウンロード、スプレッドシートへの書き込みを実行するには時間がかかりすぎてしまいエラーになってしまいます。そのため、どのようにkickflowからスプレッドシートに書き込む対象のチケットデータを取得するかが問題になりました。 ワークフローごとに異なる配列のインデックスをどのように指定するか これはZapierでの連携時からの課題と重なりますが、kickflowで作成したワークフローには、カスタムフィールドの設定が可能で、その入力データはオブジェクトに配列として保存されます。 Zapierで連携していた時は、"ticketData.inputs[10]" のようにワークフローごとに配列のインデックスを指定しデータを取得していましたが、これではカスタムフィールドの項目変更があった場合、どこのインデックスにどのデータが保存されているかの確認から行う必要がありメンテナンスコストがかかるためチケットデータから任意のデータの取得方法を改善する必要がありました。 次の章から具体的にどのような方法、仕組みで問題を解消しつつ連携を進めていったか説明します。 課題解決とGASでの連携方法 それではここから先述したZapier、GASの課題についてどのように対応し、連携をおこなったかを解説します。 まずこちらの課題について、添付のブログ内の'独自キャッシュシステムを作成'の方法を参考にしました。 課題1(Zapier)書き込み時にエラーが起こった場合、対象のチケットデータの再取得に手間がかかる。 課題1(GAS)データをどのように取得するか GoogleAppsScript(GAS)でのcacheあれこれ - 株式会社BEFOOL ブログ 実装手順解説 ①kickflowのwebhookの設定で、webhookの対象ワークフローを任意のものを選択し、webhookの送信条件を、'ticket_completed' に設定します。 この設定によりスプレッドシートで管理したいワークフローと、検知したいイベントの種類を選択できます。 ②次に、スプレッドシートにwebhookで送信された、完了したチケットのチケット番号とチケットのUUID(チケットごとに発行されるユニークな値)をスプレッドシートに記述するための関数を作成します。チケットのUUIDは後の処理で使用します GASでwebhookを受け取る方法については以下の記事を参考にしました。 Google Spreadsheet を簡易 Webサーバーとして動かして、手軽にWebHookを受け取る方法 - Qiita 作成したスクリプト const doPost = (e) => { const postData = e.postData.contents; const jsonData = JSON.parse(postData); // チケット番号とUUIDを取得 const ticketNumber = jsonData.data.ticket.ticketNumber; const ticketUuid = jsonData.data.ticket.id; //書き込み先のシートを取得 const sheetId = PropertiesService.getScriptProperties().getProperty( 'SpreadsheetId' ) const spreadSheet = SpreadsheetApp.openById(sheetId).getSheetByName( '完了済みチケットリスト' ); //スプレッドシートに書き込み spreadSheet.appendRow( [ ticketNumber, ticketUuid ] ); } kickflowからwebhookで送信された、完了したチケットの情報は画像のようにスプレッドシートに書き出されます。この処理は数秒で終了するためwebhookのタイムアウトに頭を悩ませる必要がなくなりました。 ③取得したチケットのUUIDを別のスクリプトで読み込み、kickflowのAPIを利用してチケットのデータを取得する。 使用したAPIの詳細: kickflow REST API v1 スプレッドシートからUUIDを読み込むためのコード //完了済みチケットリストからチケットのUUIDを取得 const sheetId = PropertiesService.getScriptProperties().getProperty( 'SpreadsheetId' ) const completedTicketsSheet = SpreadsheetApp.openById(sheetId).getSheetByName( '完了済みチケットリスト' ); const lastRow_completedTicketsSheet = completedTicketsSheet.getLastRow(); const ticketIdList = lastRow_completedTicketsSheet > 1 ? completedTicketsSheet.getRange(2, 1, lastRow_completedTicketsSheet - 1, 2).getValues() : [] ; GASでKickflowのAPIを呼び出すためのコード //KickflowのAPIを呼び出しチケットデータを取得するための関数 function getKickflowTicket(id) { const apiUrl = `https://api.kickflow.com/v1/tickets/ ${id[1]} ` ; const apiKey = PropertiesService.getScriptProperties().getProperty( 'KickflowToken' ); const options = { 'method' : 'get' , 'headers' : { 'Authorization' : `Bearer ${apiKey} ` , 'Content-Type' : 'application/json' } } ; try { const response = UrlFetchApp.fetch(apiUrl, options); const result = JSON.parse(response.getContentText()); return result; } catch (e) { //エラーハンドリング const adminChannel = PropertiesService.getScriptProperties().getProperty( 'corporate-it_notifications' ); const message = `KickflowAPIでのチケット情報取得時にエラーが発生しました。 \n チケット番号: ${id[0]}\n Error: ${e} ` sendMessageToSlack(adminChannel, message); } } ④取得したデータから必要なフィールドのデータを取得し、スプレッドシートに書き込む用に加工する ここでは以下の問題に対応するために新しくオブジェクトと関数を作成しました。 課題2(GAS)ワークフローごとに異なる配列のインデックスをどのように指定するか ワークフローごとにデータ取得用のkeyPathと出力時のOutput名をオブジェクトの配列で定義する 標準で設定されている項目に関しては特定のkeyが用意されているためそちらを使用します。 階層が深いものについては階層の上のkeyから配列で定義しています。 ワークフローに合わせて追加したカスタムフィールドについてはそれぞれフィールドコードが割り当てられるためそちらを使用します。 // これは購買稟議で使用しているものの一部です const keys_purchaseOrderRequest = [ { keyPath: [ "ticketNumber" ] , outputKey: "ticketNumber" } , { keyPath: [ "workflow" , "name" ] , outputKey: "applicationType" } , { keyPath: [ "title" ] , outputKey: "title" } , { keyPath: [ "authorTeam" , "fullName" ] , outputKey: "department" } , { keyPath: [ "author" , "fullName" ] , outputKey: "drafter" } , { formCode: "123456789" , outputKey: "contractStartDate" } , { formCode: "987654321" , outputKey: "contractEndDate" } , ] ここで定義したオブジェクトの配列を以下の関数で使用することで任意のデータをチケットデータから抽出することができます。 引数のticketDataにはkickflowのAPIで取得したチケットデータが、keysにはワークフローごとに設定した先述のオブジェクトの配列が渡されます。 この関数を通して任意のデータを指定したkey名で出力することができます。 //KickflowAPIで取得したチケットデータから必要なデータを抽出する関数 function extractData(ticketData, keys) { let extractedData = {} ; for ( let key of keys) { if ( 'formCode' in key) { // formField.codeを使ってデータを取得する場合 for ( let input of ticketData.inputs) { if (input.formField.code === key.formCode) { extractedData [ key.outputKey ] = input.value; break ; } } } else { // keyPathを使ってデータを取得する場合 extractedData [ key.outputKey ] = getValueByPath(ticketData, key.keyPath); } } return extractedData; } //フィールドコードが設定されていないデータについてはkeyPathを使用してデータの取得を行う function getValueByPath(obj, path) { return path.reduce((o, k) => (o || {} ) [ k ] , obj); } この手順でチケットのデータを取得することで、ワークフローのフォーマットが変更された場合でも、変更されたフィールド以外影響を受けることなくデータの取得が可能になりました。 そして、GASでチケットデータをスプレッドシートへの書き込み用に加工することで比較的自由にオブジェクトを操作できるようになり、Zapierでのデータ加工の問題もクリアされました。 課題2(Zapier)データの加工の問題 最後に ここまで読んでいただき、誠にありがとうございます。 本記事では、kickflowとスプレッドシートの連携について、またその過程で工夫した点などについて紹介しました。GASやサービスが提供しているAPIを活用することで、システム間の柔軟な連携が可能となることを実感した、貴重な経験となりました。 今後は、さらなる効率化に向けて、システムを実際に利用する社員とのコミュニケーションを密に行いつつ、社内のシステム連携を最適化していくことを目指しています
アバター
はじめに はじめまして。フロントエンド開発グループに所属している岩釣です。 スタンバイ の月間ユーザー数が1000万人を突破しました!(2023年4月末) 本記事ではそんなスタンバイのフロントエンド開発のコーディングガイドラインを紹介します。 なぜコーディングガイドラインを作るのか コーディングガイドラインを作成して運用するメリットは以下です。 コードの可読性の向上 コーディングガイドラインを規定しチーム全員がコーディングガイドラインに基づいて開発することで、コードの統一性が保たれ、コードの可読性が向上し、保守性が高まります。 チーム内でのコミュニケーションの円滑化 コーディングガイドラインが規定されている場合、チーム内でのコードの書き方についてのコミュニケーションが円滑になります。コードレビューなどでコードの書き方に関して議論する際に、コーディングガイドラインをベースにして議論することで、論点の整理や修正方針の合意が容易になります。 プロジェクトの拡張性の向上 コーディングガイドラインを規定することで、コードの再利用性が高まります。また、コーディングガイドラインに基づいて開発することで、新たな機能の追加や既存機能の変更など、プロジェクトの拡張性を向上させることができます。 開発環境 Nuxt.js 2.15 Pinia 2.0 TypeScript 4.5 ESLint 7.32 Jest Storybook 2022年11月にNuxt3の安定版がリリースされました。現在フロントエンド開発グループでは、Nuxt2からNuxt3へのバージョンアップを鋭意進めています。執筆時点ではまだバージョンアップが完了していないため、Nuxt2を対象とします。 基本方針 基本的には Vue.jsが定義しているスタイルガイド の優先度A ~ 優先度Cまでを守る すでに守れていないファイルに関しては必須とせず、リファクタリングを実施していく 新規作成ファイルにおいては全て守ること 例外的に守らなくて良いものもあるのでそれらは後述する スタンバイでは、既存のファイルに関しては適宜リファクタリングを実施していく方針にして、徐々に改善していきました。 Vue.jsスタイルガイドの中で守らないルール 単一インスタンスのコンポーネント名 違和感がないので「The」というプレフィックスを付けないことを 許容します 。 テンプレート内でのコンポーネント名の形式 ケバブケース(kebab-case)を使うことを 許容しません 。スタンバイではパスカルケース(PascalCase)に統一することで一貫性を重視しました。 ファイル命名規則 .js .tsファイルはケバブケース(kebab-case)を用いる .vueファイルはパスカルケース(PascalCase)を用いる index.vue、error.vue、default.vueのようにNuxt.jsでファイル名が固定で機能が提供されている場合を除く composables/ディレクトリに配置するファイルは以下のルールに従う use-foo-bar.tsのように「use-」から始まること ケバブケース(kebab-case)を用いること 機能が想起できる名前にすること Storybookは{対象のコンポーネント名}.stories.jsにする この辺は好みの問題で、デファクト・スタンダードが無さそうでしたので、チーム内で話し合って決めました。 Atomic Design AtomsとMoleculesの区別をつけずまとめてPartsと呼ぶ OrganismsのコンポーネントはXxxControlのようにサフィックスをControlにする TemplatesはNuxt.jsのlayoutsとみなす PagesはNuxt.jsのpagesとみなす Atomic Design とは、ウェブデザインにおけるデザインシステムの手法の1つで、複雑なUIデザインを簡単に構築するための方法論です。 Atoms(原子)、Molecules(分子)、Organisms(有機体、生命体)、Templates(テンプレート)、Pages(ページ) の5つの構成要素からなります。 Atomic Designの利点は、再利用可能でスケーラブルなUIコンポーネントを作成できることです。これにより、複数のページやアプリケーションで同じUI要素を簡単に再利用できるようになります。 また、小さな部品から大きなコンポーネントを構築することで、保守性が高く柔軟性のあるデザインシステムを実現できます。 一般的に「アトミックデザインを長期的に運用していく上で、Atoms、Molecules、Organismsそれぞれのコンポーネントの定義を明確にすることは重要です。」とされています。 しかし、運用してみるとすぐに気付くのですが、コンポーネントの定義を明確にするのはかなり難しいです。 また、「Molecules」「Organisms」のような舌を噛みそうな単語は馴染めません。 よってスタンバイでは、「Atoms」と「Molecules」の区別をつけずまとめて「Parts」とし、「Organisms」という呼称も使っていません。 Parts(Atoms, Molecules) components/parts/に置く ファイル名は再利用性を考慮して特定のロジックを想起させないことが望ましい 基本的にコンポーネント自身のmarginやpositionやz-indexを持たない(利用側でセットする) 基本的にスマホ/PC両方で利用できる(レスポンシブ) PC専用・スマホ専用のコンポーネントを作成した場合、ディレクトリpc/やsp/を作成してそこに配置する。 Partsは内部でPartsコンポーネント以外の利用不可🙅 ビジネスロジックを書かない @clickイベントや@focusイベントはemitで上位に伝える(Controllerで処理する) ButtonやRadioなどの基底コンポーネントのファイル名はプリフィックスにBaseをつける 状態を持たない(store利用不可🙅) APIコール不可🙅 Storybookを作成しすべてのpropsが操作できるようにする もしデザイナーがCSS/HTMLに熟練している場合、デザイナーにPartsを作成してもらうと分業が捗ります。 そうでなかったとしても、必ずPartsのStorybookを作成するルールにすることで、UIパーツ単体での確認が可能となります。結果、デザイナーとエンジニアの意思疎通がスムーズになります。 Controller(Organisms) components/に置く ファイル名はXxxControl.vueにする Xxxの部分はロジックを想起させることが望ましい Partsからのemitを処理する ビジネスロジックを書く Composition APIで書く できるだけComposableにロジックを切り出して、Composableの単体テストを書く 状態を持ってよい(store利用OK🙆) APIコールOK🙆 Storybookは書かなくてもOK Templates layouts/に置く(Nuxt.jsのlayoutsの機能を提供する) ファイル名はXxxLayouts.vueにする ビジネスロジックを書かない 状態を持たない(store利用不可🙅) APIコール不可🙅 Storybookは書かなくてもOK Pages pages/に置く(Nuxt.jsのpagesの機能を提供する) ビジネスロジックを書く Composition APIで書く Composableにロジックを切り出して、Composableの単体テストを書く 状態を持ってよい(store利用OK🙆) APIコールOK🙆 Storybookは書かなくてもOK Composable Composableとは Vue.jsのComposableとは、Composition APIで書かれた単一の責任を持つ関数やロジックの塊を指します。 スタンバイではcomposables/ディレクトリを作成し、ロジックをuse-logic-name.tsのように別ファイルに切り出しています。 余談ですが、Nuxt3ではcomposables/ディレクトリ内のComposableは自動importされ <script setup> 内で利用できます。 Composableに切り出すことで以下のメリットがあります。 コードの再利用性の向上 コードを再利用できます。コンポーネントで同じロジックを繰り返し書くことがなくなり、コードの保守性や拡張性が向上します。また、開発時間を短縮できます。 テストしやすいコード テストしやすいコードを作成できます。ロジックの単体テストが行いやすくなります。 柔軟なコード構造 ロジックを構造化できます。複雑なロジックを分割でき、コンポーネント内のロジックが複雑になることを防ぐことができます。また、コンポーネントの機能追加や変更に対応しやすくなります。 Composable実装ガイド composables/に置く ファイル名はuse-logic-name.tsのようにuse-から始まること ComposableはAtomic DesignのPagesとOrganismsのみが利用可能 ロジックを再利用可能な状態にしてComposition APIで実装する Composableに切り出したロジックの単体テストを書く 単一の責務の単位でファイルを分割する Composableのサンプルコード 以下は、単純なカウントアップロジックを実装したComposableの例です。 import { ref } from "@nuxtjs/composition-api" ; export default function useCount() { const count = ref(0); const increment = () => { count.value++; } return { count, increment } ; } このComposableは、countとincrementという2つのプロパティを返します。countは現在のカウントを保持するrefオブジェクトであり、incrementはカウントを1つ増やす関数です。 このComposableを使用する場合、以下のように呼び出すことができます。 <template> <div> <p>Count: {{ count }}</p> <button @click="increment">Count Up</button> </div> </template> <script> import { defineComponent } from "@nuxtjs/composition-api"; import useCount from './useCount'; export default defineComponent({ setup() { const { count, increment } = useCount(); return { count, increment }; } }); </script> このComposableの単体テストは以下のように書きます。 import { useCount } from './useCount' ; describe ( 'useCount' , () => { it ( 'increments count' , () => { const { count , increment } = useCount () expect ( count.value ) .toBe ( 0 ); increment (); expect ( count.value ) .toBe ( 1 ); } ); } ); コンポーネント実装ガイド PrettierやESLintではチェックしきれない、コンポーネントの実装に関わるガイドラインです。 HTML内で <template v-if> を多用してHTMLを制御しない 複雑な表示条件ではcomputed()で書くとシンプルになり、且つテストが可能になります。 <template v-if> を組み立てて表示を制御するのは控えましょう。 悪い例🙅 <template> <a> <template v-if="keyword">{{ keyword }}</template> <template v-if="keyword && location" > - </template> <template v-if="location">{{ location }}</template> </a> </template> 良い例🙆 <template> <a>{{ condition }}</a> </template> <script lang="ts"> import { defineComponent } from "@nuxtjs/composition-api"; export default defineComponent({ setup() { const condition = computed(() => `${keyword}${keyword && location ? ' - ' : ''}${location}`) return { condition }; }, }); </script> v-htmlを使用してHTMLを埋め込まない v-htmlを使用すると入力されたHTMLがそのまま出力されるため、XSS攻撃を受ける可能性があります。そのため、基本的にv-htmlは利用しません。 利用する場合は、HTMLとして埋め込む文字列の安全性を担保するためサニタイズします。 悪い例🙅 <template> <p v-html="htmlContent"></p> </template> <script> export default { data() { return { htmlContent: "「<b>営業 未経験</b>」のような条件でも検索できます。" } } } </script> コンポーネントのname属性 IDEやdevtoolsの補助が受けやすいので、Vueコンポーネントのnameプロパティを付与します。 良い例🙆 export default { name: "FooButton" } コンポーネントのemitイベント カスタムイベントをemitさせる際のイベント名は、ケバブケース(kebab-case)にします。 悪い例🙅 this.$emit("clickReset"); 良い例🙆 this.$emit("click-reset"); this.$parentは使用不可 this.$parentを使って親にアクセスすると子と親が密結合してしまうのでNGです。 悪い例🙅 this.$parent.foo = 'bar'; Vue.filterの使用禁止 Vue.filterはVue3で廃止されたので、Vue2のプロジェクトでも出来るだけVue.filterを利用しません。 @clickを付与していい要素 div要素やspan要素に@clickを付与してもキーボード操作時に選択できないため、基本的に@clickはbutton要素とa要素のみに付与します。 CSS実装ガイド コンポーネントの <style> にscopedを付ける Vue.jsはローカルスタイルとグローバルスタイルの混在が可能ですが、ローカルスタイルのみにします。 悪い例🙅 <style> /* グローバルスタイル */ </style> <style scoped> /* ローカルスタイル */ </style> 良い例🙆 <style lang="scss" scoped> .example { color: white; .dark { color: black; } } </style> セレクタのルール ベースとなるスタイル以外では、基本的にはClassセレクタを用いる idセレクタは使用しない(詳細度が必要以上に上がるため) 要素セレクタは使用しない(影響範囲が読みづらくなるため) 詳細度を上げすぎない Classセレクタ3段階程度の詳細度を上限目安とする(ABテスト時に上書きが面倒になるため) セレクタの数を減らすのはパフォーマンスの観点からも有用 セレクタ内で変数は用いない 重複した記述を避けられるなどのメリットはあるが、記述が複雑になるので避ける Class名のルール ケバブケース(kebab-case)を用いる 命名は省略しない(冗長でも誰が見ても分かりやすいようにするため) 悪い例🙅: <div class="bkmrk"> 良い例🙆: <div class="bookmark"> HTML要素に存在する命名を他の要素に使用しない(混乱を招くため) 悪い例🙅: <div class="section"> 状態を示すものは 「is-」「has-」などを付ける 悪い例🙅:selected、error 良い例🙆:is-selected、has-error Partsのルート要素には parts-{ファイル名}のClass名を付ける 悪い例🙅:BaseButtonコンポーネントの場合 <div class="base-button"> 良い例🙆:BaseButtonコンポーネントの場合 <div class="parts-base-button"> 細かい記述ルール 「0」の後の単位は省略する 悪い例🙅:0px 良い例🙆:0 カラーコードが省略可能な時は省略する 悪い例🙅:#ffcc00 良い例🙆:#fc0 カラーコードは小文字にする 悪い例🙅:#FACB12 良い例🙆:#facb12 親から子のスタイルを上書きしない CSSの詳細度利用してスタイルを上書きする /deep/を使わない importantでスタイルを上書きしない plugin等で導入したOSSのUIにスタイルを付ける場合は仕方ないのでOK🙆 ただし将来的にOSSのバージョンアップによって上書きしたスタイルが意味をなさなくなる可能性があるので上書きは推奨しない TypeScript実装ガイド enumは使用せずunionで定義する enumはJavaScriptへのトランスパイルの際に「即時実行関数」に変換されるので、Tree-shakingできません。 そのため、enumは使用せずunionで定義します。 const Color = { RED: "red" , BLUE: "blue" , GREEN: "green" , } as const ; type Color = typeof Color [ keyof typeof Color ] ; ESLintとPrettier ESLint と Prettier はもはや説明するまでもなく、フロントエンド開発においてデファクトスタンダードとなっています。 Vue.jsのESLintプラグイン( eslint-plugin-vue )や、TypeScriptのESLint/Prettier( typescript-eslint )を導入して自動的にコーディング規約をチェックします。 eslintrc.jsは以下を基本として、rulesで細かい調整をしています。eslintrc.jsの調整もチームで話し合って徐々に調整するのが良いでしょう。 また、いちいちプルリクエストで指摘するのは面倒なので、IDEの設定でESLintやPrettierによる整形が自動的にかかるべきです。 スタンバイではVS CodeユーザーとIntelliJ IDEAユーザーがいます。複数のIDEが使われている場合、チーム内でそれぞれのIDEの設定方法を確認すると良いでしょう。 module.exports = { extends : [ "@nuxtjs/eslint-config-typescript" , "plugin:vue/essential" , "eslint:recommended" , "@vue/typescript/recommended" , "@vue/prettier" , "@vue/prettier/@typescript-eslint" , ] , rules: { // 省略 } } 運用方法 プルリクエストのテンプレートに、コーディングガイドラインのリンクを貼る プルリクエストのレビュー観点に、コーディングガイドラインに従っているかを加える コーディングガイドラインが存在していても運用されなかったら意味がありません。 プルリクエストのテンプレートを工夫したり、プルリクエストのレビューのガイドラインも作った方がいいでしょう。 慣れてない時は細かい指摘が沢山でてくる場合もあります。レビュワーの意図が伝わりやすくするため、レビューコメントにはラベルを付けることを推奨しています。 [MUST] :必ず対応してほしい [SHOULD] :時間があれば対応してほしい [IMO] : 自分だったらこうする [NITS] : 小さな指摘点、typoとか [Q] :単なる質問 まとめ 本記事では、スタンバイで運用しているコーディングガイドラインの一部を紹介しました。 すべてを載せきれていませんが、StorybookやJestにも実装ガイドがあります。 本記事を参考に、是非コーディングガイドラインの整備と運用をはじめてみてください。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに 初めまして、株式会社スタンバイのジョブサーチメインというチームで検索エンジン周りの開発・改善に取り組んでいる金正です。 検索エンジンの改善施策の一環としてクエリオートコンプリーションシステムのリプレイスを行いました。 リプレイスに取り組んだ背景やアーキテクチャ、工夫したことなどをご紹介していきます。 クエリオートコンプリーションとは クエリオートコンプリーションとは、ユーザーの入力から始まるキーワードをサジェストする機能になっています。 以下の例では「エンジニ」という文字列から始まるキーワードをサジェストしています。 以降ではクエリオートコンプリーションのことをQAC(Query Auto Completion)と略します。 スタンバイにおけるQAC スタンバイでは、キーワード検索窓と勤務地検索窓の2つの検索窓が存在しています。 キーワード検索窓では、以下のように検索キーワードをサジェストします。 勤務地検索窓では、住所・駅のみをサジェストします。 サジェストされるキーワードに違いがあるものの、後述するシステム構成自体には特に大きな違いはないので、 後述するシステム構成等の項目ではキーワード検索窓、勤務地検索窓の両方の話をしているんだなと思っておいてください。 QACシステムリプレイスに取り組んだ背景 まずQACシステムのリプレイスに取り組んだ背景からです。 大きく以下のような課題がありました。 構築当時のエンジニアが社内に残っていない QACシステムに関するドキュメントがない QACシステムで利用しているデータやツール・ライブラリのバージョンが構築当初のままになっている QAC以外のデータも同居しているためシステムの分離ができていない 以上のように手が付けられない状態です。 既存QACシステムを改修するよりも、新しく作成しリプレイスする方が工数的にも少ないと判断し取り組み始めました。 現状のQACシステムの課題 現状のQACシステム構成は下図のようになっていました。 サジェストするデータを含んだElasticsearchのコンテナイメージを作成しECSにデプロイすることでQACを実現しています。 このアーキテクチャと前述した背景の詳細課題を列挙すると以下のような課題がありました。   課題点 詳細 1 EMRで動かすスクリプトがメンテされていないので動かない データ元のログのフォーマットやEMR自体の更新がされていないのでEMRを動かそうにも動かせない状態 2 自動化されていない インデックス更新からデプロイまで手動で行う必要がある 3 Elasticsearchのバージョンが2系 QACシステム開発当初のElasticsearchのバージョンのままになっておりセキュリティリスクがあり非常に危険 4 データ更新ができる状態ではないので、最新の検索キーワードに対応できていない 例えば、コロナと入力してもサジェストキーワードが何も表示されない 5 データ元が不明 勤務地サジェストのためには、全国の住所・駅データが必要だが、これも更新されていないので市町村合併や名称変更に追従できていない 6 ログが落ちていない ユーザーの入力キーワードや、そのキーワードに対してどのようなサジェストキーワードを掲出したか等、分析するためのログが落ちていないので改善施策を回すことができない QACのシステムアーキテクチャ 上述した課題を根本的に見直すために全システムの構成を一から見直しました。 まず最初に取り組んだのは、基幹となる検索エンジン部分の技術選定です。 QACシステムを構築する上での要件として以下のような要件がありました。 タイプ 要件 詳細 システム要件 シャーディングは不要だが、レプリケーションが必要 インデックスのデータ量は少なく(大体100MBほど)分散検索は不要だが、負荷分散や耐障害性の向上を目的としたレプリケーションが必要となる システム要件 最小限の構成 検索機能が最小限であり、QACを実現するために必要な機能があること ビジネス要件 ユーザー入力キーワードを日本語かな・漢字、ローマ字のいずれにも対応する 例えば、「とうk」とキーワード入力した際には、「東京」「東急」など「とうk」から始まるキーワードを掲出させる ビジネス要件 複合語にも対応 「エンジニア ふk」とキーワードを入力した際に、「エンジニア 副業」「エンジニア 副業 週一」などのように2つ以上のキーワードも掲出させる必要がある しかし検索エンジンといっても世には様々な検索エンジンが存在しています。 社内では他システムの検索エンジンとしてElasticsearchを利用している実績があります。 ですので、主にElasticsearchとその内部で利用されている検索エンジンライブラリであるLuceneの2つの候補が上がりました。 上記の要件を満たす前提で各検索エンジンのメリット・デメリットをまとめると以下表のようになりました。   メリット デメリット Luceneベース ・最新のLuceneに更新しやすい ・一つの機能(QAC)に特化した検索エンジンを構築できる ・システム構成が簡潔 ・トークンフィルタをカスタマイズ作成・利用可能(ローマ字変換等々) ・スケールアウトしにくい(データ量が増えた際に自前でクラスタリングを構築する必要がある) ・API層を自前で作成する必要がある Elasticsearchベース ・柔軟性がある(設定オプションが豊富でカスタマイズしやすい) ・ドキュメントが豊富 ・スケールアウトしやすい ・トークンフィルタをカスタマイズ作成・利用(ローマ字変換等々) ・最新のLuceneにすぐに対応できない(Elasticsearchのバージョンアップを待つ必要がある) ・前段のAPIサーバとElasticsearchのサーバ2台構成になるのでよりコストがかかる ・QAC以外の機能も備えているので余分な機能も存在する トークンフィルタを駆使すれば、ビジネス要件としてはLuceneベースでもElasticsearchベースでも満たすことが可能だとわかりました。 システム要件の以下2点でLuceneベースの検索エンジンを構築することに決定しました。 より軽量な構成(QACに特化した検索エンジン) Elasticsearchなどの検索エンジン側の対応を待たずに、最新のLuceneへ更新できる 以上の決定を踏まえた最終的なシステムアーキテクチャは以下のようになっています。 INDEXワークフローとQAC-APIの2箇所でLuceneを利用しています。 新システムアーキテクチャのメリット 検索エンジンのコンピューティング(以下、検索)とストレージ(インデックス)を分離したアーキテクチャにしています。 具体的には インデックスをS3のようなストレージに格納 検索時にはそのストレージに格納されたインデックスをダウンロードし、検索を走らせる このようなアーキテクチャにすることによって以下のいくつかのメリットが考えられます。 メリット 詳細 インデクシング・検索のスループット向上 CPU負荷の高いインデクシングが一度だけ行われるので、分離しないものと比較してインデクシングのスループットが向上 インデックスのバージョニングが可能 インデクシングごとに、作成されたインデックスをストレージにアップロードするタイミングでインデックスのバージョニングが可能 またインデックスのバージョンを変更する必要がある場合、インデクシングや検索システムに影響を与えることなく切り替えることが可能 メンテナンス性の向上・スケーラビリティの向上 両方のプロセスを独立してスケールさせることができる 例えば、インデクシングを行うシステムをアップグレードする場合、検索システムには影響は与えない 施策の並列実施 ランキングロジック変更等でインデックスを変更する場合、検索システムに影響を与えることなくインデクシングが完了できるので、さまざまな施策の並行稼働が容易に この検索とインデクシングを分離するアーキテクチャの考え方は、ElasticsearchやAWS OpenSearch(Elasticsearchをフォークしたサービス)でも提案されています。 Elasticsearch公式ブログ AWS OpenSearchドキュメント それぞれのシステムの詳細 次に検索エンジン以外のシステムの詳細を説明します。 ワークフロー自体は airflow で管理するなど、極力管理する工数を削減するために基本的にマネージドなAWSサービスを利用したアーキテクチャにしています。 ETLワークフロー 以下のようなシステム構成になっています。 アーキテクチャ図左端のS3に格納されているリクエストログを元に、下記に挙げる処理をETLワークフローで行います。 記号などの検索に用いるキーワードとしては不適切なキーワードまたは文字の削除 良俗公序に反するNGキーワードの削除 タイポと思われるキーワードのリライト など 実際にユーザーが入力・検索したリクエストログをもとにサジェストキーワードを生成するので、検索窓に適切なキーワードが表示されるように処理を行っています。 INDEXワークフロー 以下のようなシステム構成になっています。 上述したETLを行ったデータから、Luceneのインデックスを作成するワークフローになっています。 ECSの部分は他に、AWS GlueやAWS lambdaと候補があったのですが、コストや利用したいLuceneのバージョンなど様々な制約があったので比較的柔軟に単発バッチを実行できるAWS ECSで構成することにしています。 Luceneのトークンフィルタと自前のトークンフィルタを駆使してのインデックス構築します。 詳細は省きますが、例えば「看護師」というキーワードに対して以下ようにedge-ngram分割・ローマ字変換などの処理をし、インデックスを構築しています。 original_text: 看護師 suggest_text: 看 看護 看護師 k ka kan kang kango kangos kangosh kangoshi より詳細を知りたい方は以下の記事を参考にしてください。 スタンバイアドベントカレンダー2022向けに書いた記事 QAC-API 以下のような使い方をしています。 サーバ起動と同時に、INDEXワークフローで作成したLuceneのインデックスをダウンロードする ユーザーの検索文字列に対して、ローマ字検索するためにLuceneのトークンフィルタなどを活用している ユーザーの入力文字列に対して、Lucene インデックスに検索をかけるだけの非常に薄い構成のAPI インデックスのサイズが平均で100MBほどなので、Elasticsearchなどのようにシャーディングせず、一台のサーバのメモリ上に乗せている 性能 99パーセンタイルで、1.5msほどのレイテンシでサジェストキーワードを返せています。 インデックスのデータ量は旧QACシステムと比べて増加しているのにも関わらず、旧システムの15msと比べると10倍ほど早くなっています。 新旧で利用しているそれぞれの検索エンジンのバージョン以下のようになっています。 旧QACシステム:Elasticsearch 2.3.5 新QACシステム:Lucene 9.3.0 パフォーマンスが上がっている理由としては以下が考えられます。 サーバのCPUコア数が2倍になった 旧システムでは2vCPUに対して、新QACシステムでは4vCPUに増やした サーバにはAWS ECSを利用しているが、利用できるRAMサイズとの関係でCPUコア数を2倍に増やしている経緯がある QACだけで使われるようになたのでシンプルにリクエスト数が減った 旧システムでは、QACの用途以外でも利用されていたコンピューティングリソースをQACの用途だけで使えるようになった Luceneを直接利用することで、レスポンスを返すまでの処理量が減った 旧システムではElasticsearchにカスタムプラグインを組み込んでQACを実現していたので、処理が多数走っていた 運用 上記で説明してきた ETL・INDEXワークフロー、QAC-APIのデプロイ全てを全自動で行っているので、基本的に運用工数はかからないようになっています。 またインデックスの更新頻度は以下表のようになっており、毎日最新のQACサジェストキーワードを表示できるようになりました。 インデックス更新頻度: デイリー インデックスデータ元:過去1日分のログから 評価 新システムへのリプレイス実施を判断するため、「QACの質」と「サービス全体への影響」の2軸で旧システムとの比較評価を行いました。 QACの質 QACシステムの品質は「できるだけユーザー入力の手間を省いて、意図する検索キーワードを表示できていること」と言うことができます。 これを言い換えるとつまり、 「ユーザーの検索窓への入力数が少なく、表示されたQACサジェストキーワードのクリックが増えていること」 上記を構成する指標が以下の2つの指標になっています。 QACサジェストキーワードのクリック率 QACサジェストキーワードのカバレッジ サービス全体への影響 QACシステムの質だけではなく、スタンバイのサービス全体にとってどう影響があるのかも確認する必要があります。 スタンバイでは主に求人クリックと応募を指標として追っているので以下2つの指標を確認しています。 QACサジェストキーワードをクリックした後の求人票クリック率 QACサジェストキーワードをクリックした後の求人票応募クリック率 今後の展望 QACシステムを刷新したことでQACのログも落ちるようになり、分析することが可能になったことで、今後、以下のような改善が考えられるようになりました。 ユーザーからのフィードバックをランキングに組み込む ユーザー属性によって表示するQACサジェストキーワードを変更する インデックスのデータ量を大きくして、より広範囲のユーザー入力に対してQACサジェストできるようにする 最後に 以上、QACシステムリプレイスの概要をご紹介しました。 今回のリプレイスでは、Elasticsearchなどの単一のソフトウェアを使わずに、AWSの様々なクラウドサービスを組み合わせて検索システムを構築しました。 AWSの様々なクラウドサービスを組み合わせて検索システムを構築することによって、以下4点がこの構成をとったメリットかなと考えています。 それぞれ処理したいことに特化したサービスを利用でき、さらに何か機能を追加したい時などのカスタム性が高い 何か不具合があったときに、それぞれのクラウドサービス上で解決するだけで稼働中のシステムに影響ない またどこで何が原因で不具合が発生したのか特定しやすい マネージドサービスのため、運用工数が小さい また個人的には、既存のOSSの検索サーバを利用するのではなくLuceneを直接使うことで、検索エンジンへより深い知識と技術の習得をでき良い経験ができました。 参考文献 Elasticsearch公式ブログ AWS OpenSearchドキュメント airflow公式ドキュメント スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
こんにちは。DataPlatformグループに所属している小池です。 DataPlatformグループでは、  ●ログ計測と運用を支えるデータ基盤構築(データ基盤整備)  ●必要なデータ抽出及びモニタリング環境の整備(データ分析環境整備)  ●課題解決におけるデータ活用の支援(データ活用ソリューション) の3つを柱に、データの力でスタンバイの成長を支えています。(縁の下の力持ち) 今回は、2022年4月にリリースした簡易統計モジュールの中で 地域別給与コンテンツにおける統計値が、ユーザーの肌感覚に対して高すぎる という事象に対し、その解決までの取り組みをご紹介致します。 簡易統計モジュールとは スタンバイ において、 求職者に自分が働きたい場所の給与の状況についての 参考情報を提供するコンテンツです。 今回改修対象となった地域別給与コンテンツとは、 エリアごとの給与情報を以下のようにまとめたコンテンツです。 事の発端 2022年、夏も終わりに差し掛かったある日 1つの意見が当コンテンツ開発チームの元に寄せられた。 「給与コンテンツの、富山県の正社員年収が500万近いのは高すぎんじゃね?」 社会人経験も長い、ある富山県出身の社員からだった。 確かに、日本全体での平均給与が436万円(参考:令和元年分 民間給与 実態統計調査)なので、 自社求人データを元に集計している正社員の年収とはいえ、 地方都市にしては高すぎる感は否めない。 (富山県の皆様ごめんなさい) また、各地域で共通の集計方法を用いている以上、 富山県以外の地域でも同様の事象が発生しているとすれば、 スタンバイはユーザーに実態とかけ離れた情報を伝えていることになる。 「このままでは、スタンバイの情報の信頼性が損なわれる」 こうした危機感の下、対策の検討が始まった。 解決までのお話 解決に至るまで議論を重ね次のようなプロセスで進めた。  1. 現状の集計方法の確認  2. 問題箇所の特定  3. 改善案の検討  4. 数値評価 まず、現状の集計(各地域ごとの中央値を算出する)というロジックを確認したが Group Byで地域別に中央値を算出する、という集計ロジックであり特に問題は見当たらなかった。 集計母集団が漏れているのでは等様々なアプローチが試みられたが解決に至らず時間だけが過ぎ去った。 停滞感に支配されかかっていたある日、メンバーの一人がつぶやいた。 「肌感ってなんやねん?」 我々を向き直らせるに十分な一言だった。 この一言をきっかけに、我々は「肌感形成」についての考察から再スタートを切った。 2022年、秋も深まろうという時期のことだった。 より源流へ 〜肌感形成のプロセス考察と最適な集計対象の検討〜 残念ながら我々の周辺には、「肌感」について学術的に精通した人間はいない。 恐らく、この分野の文献を読んでも掛けた時間なりの収穫は難しいだろう。 そこで正攻法でのアプローチを諦め、自身の経験を基に大胆に以下のような仮説を打ち立ててみた。 「日々見聞きする値を基に自身の中に統計ダッシュボードが構築され、これが肌感として定着する。 」 この仮説を基に集計対象として最適な値は何かを検討し、 ここでは 「日々見聞きする値とは最も頻度の高い値、すなわち最頻値」 と考える事とした。 ここまでで、集計の流れが次のように決まった。  1. 各求人の給与の最頻値を求める。  2. その値をもとに、各地域の中央値を集計する。 だが、ここでまた1つ新たな問題にぶつかった。 最頻値を求めるには分布の情報が必要になるが、スタンバイが保有している各求人の給与データは最大値と最小値しかない (そのどちらかのみの場合もある)。 再び、メンバーの苦悩の日々が始まった。 そして解決へ 〜対数正規分布と最尤推定法〜 「そういえば、所得の分布はどのような形状になるのか?」 このような疑問を抱き、厚生労働省が公開している 所得の分布状況 を眺め 次の気づきを得た。 「最頻値が中央値よりだいぶ左だ。あと、低い方に一定限度はあるけど、高い方に明確な限度がない。これ、対数正規分布じゃないか? 」 統計学に関する書籍にも、対数正規分布の事例として年間所得が挙げられている。 更に対数正規分布は正規分布同様に再生性を有するので、前述の厚生労働省が公開している統計の基となる 各企業内の給与分布もまた対数正規分布と考えられる。 これらを基に、以下の仮説を立てた。 「各求人の初任給の分布もまた対数正規分布に近似できる」 ここで再度、各求人の給与の算出方法を確認した所、正規分布を前提とした最頻値の算出方法となっていた。 対数正規分布の最頻値に修正すれば、集計値の改善が見込まれる。問題は、確率密度関数を求める方法だ。 対数正規分布の確率密度関数の式は以下の通り。 すなわち、標本分散(σの2乗)と標本平均μの推定量が出せれば確率密度関数の導出はOKだ。 推定量の導出方法としては、未知数が関数の式の表に出ている事と 微分計算(導関数の導出)が難しくない事から、 最尤推定法 が使えそうだ。 最終的に、各求人の給与の算出式は、以下のようになった。 更に、給与の最大値、最小値のいずれかしか設定されていない求人については集計対象から外す等の 集計対象の調整を加えて、改修を完了した。 結果 各求人の給与の算出法の修正に加え、 ここでは、検証結果の一部を紹介する。 都道府県 改修前中央値 改修後中央値  (参考)政府統計 富山県 4,500,000 2,983,194 2,879,000 福島県 4,750,000 2,965,504 2,965,504 この一部に限らず、全般的に中央値が改修前と比べて現実的な値に近づく事が確認され、 本改修によって、感覚と大きくはずれない統計値を提供出来るようになった。 詳細は是非、実際にスタンバイで働きたいエリアを入力し、 検索後表示される求人一覧ページの最下部に表示される本コンテンツをその目で見ていただきたい。 最後に 本案件の難しさは 感覚的な内容を計算機で扱える形に落とし込む事 に尽きる。 統計は人間の誤った感覚や思い込みを排除して物事を判断するために使う場合もあるが、 今回は、人間の感覚値を正として感覚的な内容を数式で表現し、 計算機で扱える形に落とし込むというアプローチを取った。 今回扱う値については、ある程度年齢を重ねた方の感覚値が正しいように思われたからだ。 また、使えるデータの量が少なかった事や関連の専門知識が不足していた事も解決を困難にした。 この点については「今あるものが最強の武器」と開き直り仮説思考で乗り切った。 今後も、人の感覚に寄り添う統計と補正する統計を上手に使い分け大胆な仮説をもとに 必要であれば他の数学分野の知見、更に社会科学や心理学といった他の学問分野の知見を活用して データに意思を与え、ユーザーにより価値のある情報を提供していきたい。 補足 ここではストーリー中に登場した統計用語について簡単に解説する。 正規分布と対数正規分布 まず正規分布について簡単に説明する。 この分布は自然界や社会現象の多くで現れる確率分布であり、 平均値μが中心となり、標準偏差σが広がりの度合いを表す。 尚、確率密度関数とグラフは以下のようになる。 次に対数正規分布について説明する。 この分布は、正規分布に従うランダムな変数の対数が従う分布で、 特徴は正の値を取ることと歪みがある分布です。 本記事で扱ったように経済学の分野で収入分布を表すのに用いられる他 金融分野や生態学、医療分野などで使用され、例えば、経済成長率や生物種の体サイズ分布などを表すのに使用される。 尚、確率密度関数とグラフは以下のようになる。   最後に、正規分布と対数正規分布の使い分けについて説明する。 世の中には、 平均値からのバイアス(ズレ)が和の形でかかる事象と積の形でかかる事象 が存在し、これらの事象に対して大まかに以下のような使い分けとなる。  ●平均値からのバイアスが、和の形でかかる場合(x=μ+Σε):正規分布  ●平均値からのバイアスが、積の形でかかる場合(x=μ×Πε):対数正規分布 まず、平均値に対してバイアスが和の形でかかる事象の例として 工場のラインで製造された製品寸法について考える。 これは、設計の狙い値(=平均値)に対し加工に伴う誤差が和の形でかかるケースの例であり 製品寸法の分布は正規分布がよくマッチする。 製造の現場では、あるラインがどれだけ決められた規格内で製造出来ているかを 図る指標として工程能力指数((規格上限 - 規格下限) / 6×製品寸法の標準偏差)を 用いるがこれは製品寸法の分布が正規分布であることを前提とした指標です。 もう1つの例として平均値に対してバイアスが積の形でかかる事象を考える。 この場合、生データで分布を描くと正規分布と比べて右に裾野の広い分布となる。 今回扱った年収は、ある基準値に対して前職の実績及び経験年数等に応じて何倍という バイアスがかかっていると考えられる。 (この辺りの知見をお持ちの方がいらっしゃったら、是非教えていただきたいです。) 尚、対数変換を施した値の分布は正規分布になる。 (対数変換によって積は和に変換される為) 最尤推定法 確率密度関数におけるパラメータ推定(今回の場合、正規分布、対数正規分布におけるσ、μの推定)を行う方法の1つとして、 今回使用した最尤推定法がある。 ざっくり説明すると想定した確率密度関数を基に尤度関数を定め、この関数値(尤度)が最大となるときの パラメータ値を最尤推定量として求める、といった流れになる。 詳しくは以下参考文献を参考にしていただきたい。 参考文献 ●統計学入門(東京大学 教養学部 統計学教室 編) ●日本統計学会 公式認定 統計検定1級 対応 統計学(日本統計学会 編) ●仮説思考 BGC流 問題発見・解決の発想法 (内田和成 著) スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
こんにちは。スタンバイでQuality Assurance(以下QA)を担当している樽井です。 我々QAグループはプロダクトの品質を守り高める存在として、日々の品質業務を改善するためにデータを活用しています。 ここではデータの概要と実際の活用事例をご紹介していきます。 スタンバイのプロダクト開発とQA体制 スタンバイでは、求人検索エンジン「スタンバイ」のWeb・Native Appの求職者向けサービスに加え、求人入稿や広告管理などの企業向けサービスを運営しています。サービスごとに担当開発グループが決まっており、多くのグループは1週間〜2週間単位のアジャイル開発で作業を進めています。 対するQAグループは6名で、複数開発グループからの検証依頼を効率よくこなす必要があります。「特定のメンバーに検証が偏っていないか」「検証の改善ポイントはないか」を中長期的に確認していく上で、データを活用できないかと考えました。 実際にやったこと 検証業務全体のデータを取得 元々は本番障害、検出不具合のみを集計していましたが、検証業務全体を捉えられるようにデータ取得範囲を広げています。 現在取得しているデータ群  ・本番障害  ・検出不具合  ・検証依頼  ・作業工数  ・業務知識 データ元はJira、Zube、Slackなどバラバラです。それらをGoogle スプレッドシートに集約後、データの最終加工をして、Looker Studioへダッシュボードとして出力しています。 図1:Looker Studio仮想データ出力例 月次の数値振り返り会を開催 ダッシュボードは日々、データの急変動がないか、発生した出来事によってどのデータが変わったかを確認しています。 また、毎月頭に先月の数値振り返り会を実施しています。QAメンバー全員が揃い、その月の出来事や忙しさの肌感など定性的な素材と、実際に出た数値の定量的な素材を照らし合わせて、意見交換を行います。 例えば「今月の本番障害が多いのはなぜか」「急に残不具合が増えたのはなぜか」を複数人で話すことにより、「この観点が漏れていた」「この機能の不具合が多く出ていた」「差し込みの案件で優先度が下がった」など、より多くの視点で捉え、実施者が検証中に感じた思いを全員で共有できます。 加えて、過去データを元に「この時期は検証依頼が増えるから細かいものは先に取り掛かっておこう」「この観点で不具合が増えてきているため設計段階でクリアにしておこう」といった直近の作業について話し合いをしたり、データの不明点について議論します。 数値振り返り会内で解決しなかった疑問・調査事項はNext Actionとして担当を決め、不明点が明確になるように努めます。 データ活用事例 事例1:忙しさメーター QA業務の忙しさは、自分たちの感覚を可視化することから始まりました。案件のボリューム&難易度は、ストーリーポイントとして数値化しています。 現在はテスト設計終了時に設計担当者がポイントをつけ、検証終了後にQAメンバー全員でポイントの見直しをする方法を採用しています。 この忙しさメーターにより、「先月は何だか忙しかった」「来週は忙しくなりそう」を可視化できるようになってきました。自分たちの感覚で捉えていたものを他の人にもわかる形にできたことで、共通認識を持てるようになりました。 図2:忙しさメーターの出力例 事例2:業務知識シート QAメンバーは担当する開発グループ・システムが固定化される傾向にあり、知見が属人化している状態でした。 各システムに対して検証に利用するスキルを一覧化し、どのスキルを身につけたのかを毎月チェックしています。スキルは「XXログの取得」「XXシステムの○○操作」「XXシステムのテスト項目作成」の様に分かれています。スキル習得の推移を追うことで、自分自身の成長やQAグループ全体の変化を確認しやすくなりました。「この案件はAさんとBさんが良いね」「この知識が属人化しているから勉強会で共有しよう」など、案件割り振りや教育戦略も立てやすくなりました。 図3:業務スキルシートの出力例 例えば、左図のAさん業務知識を見てみると、4月時点で求人作成サービスの知識は0です。 求人作成サービスに関するスキルは、求人管理のシステム操作がメインになります。この時は複数メンバーが同様の状態であり、特定メンバーに知見が偏っていることがわかりました。 そこでハンズオンの勉強会を実施し、まずは利用頻度の高い「求人作成」スキルを取得してもらいました。このスキルを起点として「求人編集」「会社作成」と次のスキルを身につけていき、12月には求人作成サービスだけでなく、企業向けサービスのスキルも伸ばすことができました。 また、右図のQAチーム業務知識では、個人の業務知識を総合した値を見ています。4月から右肩上がりとなっていたグラフですが、9月は人員入替によりが下がってしまっています。 このような場合、直近スキルを習得したメンバーが教える側に回ることで、新規メンバーのフォローアップと教える側の知識の定着を図ってきました。結果として、以前は5ヶ月間(4〜8月)かけて達した水準に、4ヶ月間(9〜12月)で近づくことができました。 データ活用による変化 検証工数の拡大と不具合検出の増加 データ活用を始めて3年が経過し、ようやく成果らしきものが見えてきました。 3年前に比べ、QAメンバーの全工数におけるテスト工数割合が上昇傾向にあることがわかってきました。これはQA業務の本質である、検証作業に割ける時間が増加していることを示しています。 これまでは、普段担当しないシステムの検証を実施する際、都度インプットを行ったり、知見のあるメンバーが他業務と並行して全面サポートをしたりしていました。現在は各サービスの知見を事前インプットしているため、忙しいタイミングでの説明工数を短縮でき、各メンバーが割り振られた案件に集中できるようになってきています。 工数全体を見ても、一人当たりの稼働時間が増えたわけではないことから、工数を抑えつつ、検証に割く時間を増やせていると言えそうです。 図4:テスト工数割合の出力例 また、検出不具合数も着実に増えています。 こちらは1案件につき平均何件の不具合を検出しているのかを算出したグラフです。不具合内容を精査したところ、過去に知識不足で検出できなかった不具合の増加により、全体の検出数を引き上げている可能性が高いとわかりました。 QAメンバーが持つ知識を意図的に広げていくことで、必要な知識や観点を持って検証にあたることができていると考えられます。 図5:不具合検出率の出力例 今後の野望 新たな課題が発生した時、データを活用することで、客観的な視点で、Next Actionを考えるヒントを得ることができました。これまで目に見えなかった忙しさや頑張り・知識を可視化し、成果の共有ができ始めています。 今はまだQA内部での活用に留まっているデータを、今後はプロダクト関係者、さらには全社的な品質指標へと昇華していきたいと考えています。 スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
株式会社スタンバイでプロダクトオーナーを務めている上野です。 「スクラム導入したけど、開発が上手く回らない」「これで良いのかわからない」。スクラム開発でお困りの方も多いのではないでしょうか。僕の所属するチームもスクラムが上手 くいかずに苦しんだ時期がありました。 スクラムガイド って守るべきことが書いてあるだけで、運用方法までは書いてないんですよね。少なくともこのあたりは押さえておけば「スクラム赤点」にはならず、軌道に乗るんじゃないかな、という部分をお伝えします。 スクラムとは 僕も当初そうでしたが、スクラムは誤解されやすいフレームです。スクラムガイドがあることからも、「単純にこのガイドラインを守っていれば良いのだな」となりがちな気がしています。 しかし、スクラムは「 現状を素早く正確に把握し、課題を炙り出すフレームワーク 」です。少々乱暴に言えば「目標と現状のギャップを全部見える化し、ギャップを解消することで前進していく」というフレームワークです。導入後も常にチーム全員の努力がない限り成り立ち得ない世界なのです。 そもそも、拠り所となるスクラムガイドに記載されていることといっても、基本的なスクラムイベントやスクラムにおける考え方や価値観に関することくらいです(ただし、心理学や行動学からしっかりと裏付けされた理論)。当然、環境が異なるチームがフレームワークを使うので、唯一の正解となるスクラムの形もありません(「間違った形」は定められそうですが)。 スクラムガイドというと、一般的にスクラムイベントや役割定義に目が行きがちですが(僕自身そうでした)、実はここに記載されている「考え方」がスクラムの成否にかかっていることはあまり知られていないかもしれません。 以前、複数の企業でスクラム導入支援している外部コーチに伺ったところ、スクラムがうまく機能しないケースのうち、最も多い理由が、この 「考え方」の認識が揃っていないこと 、だそうです。 こうした「考え方」部分が重要なのは、スクラム開発がチーム活動であり、チームで助け合うことを前提に作られているフレームワークであるからでしょう。例えば、スプリント内のチケットが早めに完了したメンバーがいたら、他に困っているメンバーの開発サポートに回ったり、POの時間が取れない場合に開発メンバーがチケットの作成まで行なうことなど、ほかにも色々ありえます。 まさにスポーツのように、1つの目標に向かってチームが協力して行う活動は単なる機械的な仕組みだけではなく、「考え方」、いわゆるマインドやスタンスが肝になってきます。 目線のすれ違う開発チーム 当時、僕の所属していたスクラムチームはそれぞれが別の方向を向いている状態でした。 <当時の課題メモ> スプリント内のチケット達成率に対する温度差が生まれてしまっている 個々人で開発の進め方があり、守るべき部分と妥協部分の違いなどが明確にされないまま属人的に進んでしまう レトロ等の振り返りの場でも何を振り返ればよいかわからない状態で、結果的に何も出ない 今思えば、タスクやサブタスクのスプリント内完了の意識を徹底できておらず、開発メンバーがそれぞれの状況を把握できず、それゆえ、協力するような会話ができていないという状態でした。 お互い期待することが異なるため、開発上のストレスが増します。やがて目に見えないストレスはいずれ大きな溝となり、最悪の場合、スクラムチームの崩壊に至っていたでしょう。 幸い、チームの崩壊とまでは至りませんでしたが、パフォーマンス的には”赤点”でした。チーム内から「なんでスクラムやってるんだっけ?」という問いかけが出たほどです。 この状況を打開すべく、チーム外部のコーチ(社内のアジャイルコーチ)の力もお借りし改善に移りました。具体的には、開発の「考え方」を統一させるワークを複数回行いました。週1回程度で1−2時間、1ヶ月程度かけ、主に今から説明する2つの考え方をチームメンバー全員で理解、解釈し、ワーキングアグリーメント化するという活動です。 ワーキングアグリーメントを作って終わり!では意味がないので、チームでの取り決めを日々の、習慣にさせるため、さらに数ヶ月の時間をかけました。 結果、開発チームの雰囲気や生産性は赤点を脱するどころか、組織として次のフェーズに進んだ印象を持てました。スプリント内のチケット進捗率、Slackのコミュニケーション量、普段交わされる話題の数、そして何より、レトロスペクティブによる課題出し、トライの実践、振り返りまでのサイクルが増えたことに驚いています。課題以外にもスプリント内で「良かった取り組み」を付箋化するのですが、この量も当時から比較すると3,4倍違うのではないでしょうか。 スプリントが上手く回るような大それた仕組みを新たに作ったわけではないです。単純に「考え方」を統一しただけです。 この実践を通じて、改めてスクラムメンバーで「考え方」を統一することの重要性を認識しました。「考え方」をいかにチームで合意し、浸透させ、実践するか。これが成功の秘訣であると考えています。チームのカルチャーによってtipsやルールの違いが滲み出るはずで、それがまた面白いところなんだろうなという気がします。前置きが長くなりましたが、以下にて、スクラムで”赤点”にならないための、重要な考え方を2つご紹介します。 1.スクラムの理論 1つ目は、スクラムガイドで「スクラムの理論・スクラムの三本柱」として扱われている考え方です。主に「Inspection(検査)」「Adaptation(適応)」「Transparency(透明化)」の要素から成ります。 少し噛み砕くと ・常に現状に対して自分たちのあるべき状態を常にアップデートし(検査) ・現状とあるべき姿への差分を埋め続ける努力をしていること(適応) ・そもそも正しい状況判断を行えるように、活動を可視化しよう(透明性) というのがスクラムの価値観です。 つまるところ、目標に対して現状はどのような状態なのか、そのギャップを埋めるために何ができるか、が明らかになっていること、ビジネスシーンでよく目にするような「As-is、To-be、To-do」のフレームワークと同じような考え方ですね。 つまり、スプリント内のあらゆる活動において目標が設定され、現状差分に対する仮説を持ち、(可能なら)計測され、公開されるなど、常に振り返れる状態を作り続けないといけません。例えばチケットには「完了基準(Acceptance Criteria)」のような明確なゴールを設ける、スプリント内で消化したストーリーポイントを計測していく、スプリント内で用意できる作業時間を明らかにするなどが該当します。 僕のチームでは、1つ目の「考え方」としてこのスクラムの理論を理解する、というステップから始まりました。 2.スクラムの価値基準 もう1つは、5つのスクラムの価値基準「Commitment(確約)」「Focus(集中)」「Openness(公開)」「Respect(尊敬)」「Courage(勇気)」です。 これだけでも十分なケースもあるかもしれませんが、僕のチームではもう少しアクションに落とし込みたかったので、以下のようにスプリントのワーキングアグリーメントとして設定しました。数が多くても守りづらいので、まずは3つに絞っています。 ポイントは、第三者が見ても客観的に評価できる内容かどうか、です。 ※参考①:miroでのワークショップのアウトプット ※参考②:miroでのワークショップのアウトプット あくまで、上記は僕のチームのカルチャーや課題感から滲み出てきた解釈であり、一例です。他チームで実施すれば別の解釈があると思います。 以上、簡単ではありますが、スクラムの赤点対策に必要な2つの「考え方」をご紹介しました。スクラムで違和感を感じたら、チケットよりも、プロダクトビジョンよりも、「メンバーの目線はあっているか?」を意識してみてください。 赤点を脱した今、個人的には、関わっているメンバー全員が生き生きしているか、楽しんでいるか、を常に意識するようにしています。 まとめ スクラムは「現状を素早く正確に把握し、課題を炙り出すフレームワーク」である 個人技ではなく、チーム活動であることを忘れない スクラムガイドの「スクラム理論」「価値基準」をチームで解釈し、「考え方」を統一することが赤点対策に有効 参考資料 スクラムガイド日本語版(2020年) https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf   スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
株式会社スタンバイでプロダクトオーナーを務めている上野です。 「スクラム導入したけど、開発が上手く回らない」「これで良いのかわからない」。スクラム開発でお困りの方も多いのではないでしょうか。僕の所属するチームもスクラムが上手 くいかずに苦しんだ時期がありました。 スクラムガイド って守るべきことが書いてあるだけで、運用方法までは書いてないんですよね。少なくともこのあたりは押さえておけば「スクラム赤点」にはならず、軌道に乗るんじゃないかな、という部分をお伝えします。 スクラムとは 僕も当初そうでしたが、スクラムは誤解されやすいフレームです。スクラムガイドがあることからも、「単純にこのガイドラインを守っていれば良いのだな」となりがちな気がしています。 しかし、スクラムは「 現状を素早く正確に把握し、課題を炙り出すフレームワーク 」です。少々乱暴に言えば「目標と現状のギャップを全部見える化し、ギャップを解消することで前進していく」というフレームワークです。導入後も常にチーム全員の努力がない限り成り立ち得ない世界なのです。 そもそも、拠り所となるスクラムガイドに記載されていることといっても、基本的なスクラムイベントやスクラムにおける考え方や価値観に関することくらいです(ただし、心理学や行動学からしっかりと裏付けされた理論)。当然、環境が異なるチームがフレームワークを使うので、唯一の正解となるスクラムの形もありません(「間違った形」は定められそうですが)。 スクラムガイドというと、一般的にスクラムイベントや役割定義に目が行きがちですが(僕自身そうでした)、実はここに記載されている「考え方」がスクラムの成否にかかっていることはあまり知られていないかもしれません。 以前、複数の企業でスクラム導入支援している外部コーチに伺ったところ、スクラムがうまく機能しないケースのうち、最も多い理由が、この 「考え方」の認識が揃っていないこと 、だそうです。 こうした「考え方」部分が重要なのは、スクラム開発がチーム活動であり、チームで助け合うことを前提に作られているフレームワークであるからでしょう。例えば、スプリント内のチケットが早めに完了したメンバーがいたら、他に困っているメンバーの開発サポートに回ったり、POの時間が取れない場合に開発メンバーがチケットの作成まで行なうことなど、ほかにも色々ありえます。 まさにスポーツのように、1つの目標に向かってチームが協力して行う活動は単なる機械的な仕組みだけではなく、「考え方」、いわゆるマインドやスタンスが肝になってきます。 目線のすれ違う開発チーム 当時、僕の所属していたスクラムチームはそれぞれが別の方向を向いている状態でした。 <当時の課題メモ> スプリント内のチケット達成率に対する温度差が生まれてしまっている 個々人で開発の進め方があり、守るべき部分と妥協部分の違いなどが明確にされないまま属人的に進んでしまう レトロ等の振り返りの場でも何を振り返ればよいかわからない状態で、結果的に何も出ない 今思えば、タスクやサブタスクのスプリント内完了の意識を徹底できておらず、開発メンバーがそれぞれの状況を把握できず、それゆえ、協力するような会話ができていないという状態でした。 お互い期待することが異なるため、開発上のストレスが増します。やがて目に見えないストレスはいずれ大きな溝となり、最悪の場合、スクラムチームの崩壊に至っていたでしょう。 幸い、チームの崩壊とまでは至りませんでしたが、パフォーマンス的には”赤点”でした。チーム内から「なんでスクラムやってるんだっけ?」という問いかけが出たほどです。 この状況を打開すべく、チーム外部のコーチ(社内のアジャイルコーチ)の力もお借りし改善に移りました。具体的には、開発の「考え方」を統一させるワークを複数回行いました。週1回程度で1−2時間、1ヶ月程度かけ、主に今から説明する2つの考え方をチームメンバー全員で理解、解釈し、ワーキングアグリーメント化するという活動です。 ワーキングアグリーメントを作って終わり!では意味がないので、チームでの取り決めを日々の、習慣にさせるため、さらに数ヶ月の時間をかけました。 結果、開発チームの雰囲気や生産性は赤点を脱するどころか、組織として次のフェーズに進んだ印象を持てました。スプリント内のチケット進捗率、Slackのコミュニケーション量、普段交わされる話題の数、そして何より、レトロスペクティブによる課題出し、トライの実践、振り返りまでのサイクルが増えたことに驚いています。課題以外にもスプリント内で「良かった取り組み」を付箋化するのですが、この量も当時から比較すると3,4倍違うのではないでしょうか。 スプリントが上手く回るような大それた仕組みを新たに作ったわけではないです。単純に「考え方」を統一しただけです。 この実践を通じて、改めてスクラムメンバーで「考え方」を統一することの重要性を認識しました。「考え方」をいかにチームで合意し、浸透させ、実践するか。これが成功の秘訣であると考えています。チームのカルチャーによってtipsやルールの違いが滲み出るはずで、それがまた面白いところなんだろうなという気がします。前置きが長くなりましたが、以下にて、スクラムで”赤点”にならないための、重要な考え方を2つご紹介します。 1.スクラムの理論 1つ目は、スクラムガイドで「スクラムの理論・スクラムの三本柱」として扱われている考え方です。主に「Inspection(検査)」「Adaptation(適応)」「Transparency(透明化)」の要素から成ります。 少し噛み砕くと ・常に現状に対して自分たちのあるべき状態を常にアップデートし(検査) ・現状とあるべき姿への差分を埋め続ける努力をしていること(適応) ・そもそも正しい状況判断を行えるように、活動を可視化しよう(透明性) というのがスクラムの価値観です。 つまるところ、目標に対して現状はどのような状態なのか、そのギャップを埋めるために何ができるか、が明らかになっていること、ビジネスシーンでよく目にするような「As-is、To-be、To-do」のフレームワークと同じような考え方ですね。 つまり、スプリント内のあらゆる活動において目標が設定され、現状差分に対する仮説を持ち、(可能なら)計測され、公開されるなど、常に振り返れる状態を作り続けないといけません。例えばチケットには「完了基準(Acceptance Criteria)」のような明確なゴールを設ける、スプリント内で消化したストーリーポイントを計測していく、スプリント内で用意できる作業時間を明らかにするなどが該当します。 僕のチームでは、1つ目の「考え方」としてこのスクラムの理論を理解する、というステップから始まりました。 2.スクラムの価値基準 もう1つは、5つのスクラムの価値基準「Commitment(確約)」「Focus(集中)」「Openness(公開)」「Respect(尊敬)」「Courage(勇気)」です。 これだけでも十分なケースもあるかもしれませんが、僕のチームではもう少しアクションに落とし込みたかったので、以下のようにスプリントのワーキングアグリーメントとして設定しました。数が多くても守りづらいので、まずは3つに絞っています。 ポイントは、第三者が見ても客観的に評価できる内容かどうか、です。 ※参考①:miroでのワークショップのアウトプット ※参考②:miroでのワークショップのアウトプット あくまで、上記は僕のチームのカルチャーや課題感から滲み出てきた解釈であり、一例です。他チームで実施すれば別の解釈があると思います。 以上、簡単ではありますが、スクラムの赤点対策に必要な2つの「考え方」をご紹介しました。スクラムで違和感を感じたら、チケットよりも、プロダクトビジョンよりも、「メンバーの目線はあっているか?」を意識してみてください。 赤点を脱した今、個人的には、関わっているメンバー全員が生き生きしているか、楽しんでいるか、を常に意識するようにしています。 まとめ スクラムは「現状を素早く正確に把握し、課題を炙り出すフレームワーク」である 個人技ではなく、チーム活動であることを忘れない スクラムガイドの「スクラム理論」「価値基準」をチームで解釈し、「考え方」を統一することが赤点対策に有効 参考資料 スクラムガイド日本語版(2020年) https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf   スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター
はじめに こんにちは、Tech blog運営担当の青山です。 スタンバイには、プロダクトの行動指針「START」に基づいて、素晴らしい成果を創出したメンバーを表彰する「START賞」という表彰制度(月次表彰)があります。 また、半期(上期・下期)ごとに、プロダクト部門所属のメンバーの中から1名に「ベストプレイヤー賞(プロダクト部門)」が贈られます。 (行動指針「START」の詳細は、 「スタンバイのプロダクト本部の行動指針「START」と「Engineering Belt」とは?」 にてご確認ください) 本記事では、入社1年未満にもかかわらず、「6月度START賞 MVP」「10月度START賞 準MVP」「FY22上期 ベストプレーヤー賞(プロダクト部門)」を受賞し、プランナーとして大活躍中の城本さんに、高評価を獲得したプロジェクト「Apply URL プロジェクト」について、インタビューをしていきたいと思います! -早速ですが、「Apply URL プロジェクト」について、概要を教えてください。 城本: 一言で伝えると「ユーザーの応募体験向上」のためのプロジェクトです。 以前、ユーザーインタビューを実施した際に、求職者様より「スタンバイの求人詳細ページの「応募ボタン」を押すと、掲載元の求人詳細ページに遷移してしまう。すぐに応募したいと思っている分、応募フォーム以外のページに遷移するのは分かりにくいし、不便」などのお声をいただいていました。 そのお声をもとに、求人詳細ページの「応募ボタン」を押した後は、掲載元の応募フォームに直接遷移させることを「ユーザーの応募体験向上」のための初手だと考えました。 また、クライアント企業(求人掲載元の企業)様にとっても、求人検索の利便性を高めることは、「企業様と求職者様とのマッチング」の機会を増やすことに直結します。比較的シンプルなステップで企業様が導入できる「Apply URL」は、まさにファーストステップとしてうってつけでした。 -ありがとうございます。求職者様の応募体験を向上させることで、「スタンバイで求人を検索し、応募に進む機会」をより多く提供していきたいですね。次に、多くの賞を受賞することになった「Apply URL プロジェクト」における城本さんの具体的役割を教えていただきたいです。 城本: プランナーの基本的な役割は、施策のROIの見立て、要求・要件の整理、スケジュール管理などです。それに加え、スタンバイには様々なバックグラウンドを持ったプランナーが集まっており、エンジニア出身、デザイナー出身、コンサル出身など各人のスキルによって得意分野が異なり、プロジェクトの内容によっても、プランナーとしての役割は多少変化します。 「Apply URL プロジェクト」においては、機能をリリースするだけでなく企業様に導入いただくことまでを視野にいれる必要があるので、導入率向上を含めた戦略の立案も私の大事な役割でした。あまり具体的に表現するのが難しいのですが、プランナーの役割は、「プロジェクト成功にむけて、開発実装以外はなんでもする人」と言えばよいかもしれません。 -「開発実装以外なんでも」・・・聞いただけでも大変そうですが、その分やりがいがありますね。「Apply URL プロジェクト」のステークホルダーについても教えてください。 城本: 開発部門内だと、自グループ以外の4つのグループが関わっていました。また、それ以外では法務、セールス、CSのグループにご協力いただきました。 -自グループにとどまらず、他に7つのグループと関わりながらプロジェクトを推進されたのですね。城本さんが表彰された大きな理由の1つは、「“Apply URL プロジェクト”をスムーズに進められた」ことだと伺いました。これだけ多くのグループが関わったプロジェクトを成功に導くための「プロジェクト推進術」を知りたいです。 城本: そうですね、まず前提として、過去の経験と比較しても、スタンバイのエンジニアは開発のスピードが速く、プロジェクトにおける提供価値の擦り合わせが完了すれば、要求を基に要件・仕様の提案などを積極的に行って自走していく優秀な方が多いと感じます。また、私のような非エンジニア人材に対しての技術的な説明も分かりやすいので、プランナー側でも設計意図を理解しながら実装を進めることができます。 技術・意識ともにプロフェッショナルな方々と共に進められたことが、プロジェクト成功の大きな要因といえますが、それ以外で「プロジェクト推進」において上手くいった要因を挙げるとするならば、振り返ると3つほどあると思います。 1.40%の完成度でもいい。たたき台を作成する 私には、数値分析やデザインなどの専門性はありません。完成度の高い資料の作成は難しいですが、たたき台やワイヤーフレーム、分析要件など40%の完成度でもいいので、まずはとにかく形にすることを心がけています。 それを基にお願いしたいことを説明すれば議論がしやすくなり、意思伝達のスピードが上がります。自分にないスキルは積極的にスキルのある人や専門グループを頼らせてもらい、40%のたたき台を80%以上のクオリティに仕上げるのです。 また、その40%のたたき台の中にも「ぶれない箇所・要素」はあるので、他グループへの相談を併行しながら、要件整理を進めていきます。これは、コミュニケーションの効率化とプロジェクトの推進スピードの向上に多少ながら寄与したと考えています。 2. なぜやりたいのかを論理的に全力で説明する 「Apply URL プロジェクト」に関しては、当初ステークホルダー全員がポジティブな想いを持っている状態とは言えませんでした。なぜなら、課題に対して多くの解決策がある中で、それがベストな打ち手といえる認識を統一できていなかったからです。 そこで、「仮説の数値を基にした効果」や「他の手段と比較した結果」などを踏まえ「なぜやりたいのか」を論理的に説明することに力を割きました。さらには、このプロジェクトは、「中長期的な視野で、将来につながる」プロジェクトであることを理解してもらうことも欠かせませんでした。 また「Apply URLが今選択しうるベストな打ち手である」というストーリーを考えることは、私にもできることですが、「誰から伝えるのか」は、ストーリーの納得感を高めるためにも、とても重要です。そのため、最終的にはマネージャーやCOOにも協力を仰いで関係者との認識合わせを行いました。 批判や反対意見は、裏を返せばスタンバイを成長させたいという想いがあるからこそ出てくる、有り難い言葉です。そういった意見からプロジェクトのロードマップや機能仕様を適切に変更することができた経験もあります。目線のずれている箇所を洗い出し、すり合わせることに全力で向き合うことで、プロジェクトメンバーの一体感を高めることができたのではないでしょうか。 3.QCDSに沿って、プロジェクトを管理する 最後に基本的なことですが、プロジェクト管理においては、「QCDS」(品質:Quality、予算:Cost、納品:Delivery、スコープ:Scope)という管理指標があります。 プロジェクトマネジメントをされている方にとっては当たり前のことだと思いますが、私は基本に忠実に「QCDS管理」を大切にしています。 また、スタンバイのフェーズではMVP(Minimum Viable Product)開発も意識する機会が多いです。MVPとは、一般的に「顧客にとって、価値が提供できる最小単位」として要件を定義することですが、リリースに必要なスコープを絞ることでデリバリーを早め、機能追加・改善のサイクルを早くまわすことができます。 実装が進むにつれて工数の見積もり精度も高くなるため、「リリースするに足る最小単位」の基準を決めておくことで、リリース日の延期が必要かどうかを早めに判断できます。 さらに、開発を進めていく中では、「あれもやりたい」「これもやりたい」ということが、常に出てきます。このような場面においても、プロジェクトのMVPやQCDS基準を基に議論することで、「デリバリー期日に間に合う範囲で、このWANT要件を追加できないか」などのスコープ調整の提案もしやすくなります。 プロジェクトが進行する間、共通の判断軸を基に議論することで、結果として手戻りを少なくし、プロジェクトの進行スピードを一定に保つことができます。 -ありがとうございます。言うは易し行うは難しですが、3点とも城本さんは、ごく自然に行動されているなと感じました。上記のような「プロジェクト推進術」をお持ちの城本さんでも「Apply URL プロジェクト」において、「スムーズに進まないこと」や「スムーズに進まなさそうなリスク」はなかったのでしょうか? 城本: 機能をリリースすることに関しては、あまりスムーズにいかないという場面はなかったと思います。ですが、「Apply URL」は、ただ作っただけでは意味がなく、企業様に導入いただかないと求職者様に体験していただけません。導入する・しないはもちろん企業様が判断されるので、導入が進まずに作ったものが無意味になる事態は、このプロジェクトにおいて私の力では調整がしにくいリスク要素だと感じていました。 そのため、セールス部門への相談やヒアリングは丁寧に行いました。機能に対して好意的な印象を持っている企業様や、導入可能性のある企業様とのコミュニケーションを代替していただき、リリースのタイミングについても、他の施策との抱きあわせで、企業様が「このタイミングならまとめて対応できる・導入しやすい」という時期にデリバリー時期をあわせることをプロジェクトゴールに含めました。良いプロダクト作りは開発だけの視点では、成し得ません。開発・企業・ユーザーにとって「三方が良い状態」を目指すことができたことも、プロジェクト成功に起因していると思います。 -ありがとうございます。関係者に最大限に配慮する想いと行動が、プロジェクトをスムーズに進め、成功に導いているんだと感じました。 私自身も城本さんがリードするプロジェクトに関わっていますが、「スムーズ」と感じる裏には、城本さんのきめ細かい配慮が隠れていたことに気づきました。貴重なお話をありがとうございました! スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com
アバター