dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

クラシル検索品質評価ガイドラインを作成しました

f:id:sakura818uuu:20181022182127p:plain

こんにちは.開発部のsakura ( @818uuu )です.
普段はレシピ動画サービス クラシルの検索を担当しています.

今回はクラシルの検索品質評価ガイドラインを作成したお話を書きます.

はじめに

みなさんは何かを検索した時に「思った結果とちょっと違うな……」と感じた経験があると思います.そういう検索の精度や品質管理はどのように保ち,また,改善されているのでしょうか.
Googleだと General Guidelines という検索品質評価ガイドラインがあり,それに基づき多くの評価者が検索品質の評価を行っています.
今回クラシルでもその試みに取り組んでみようと思いクラシルの検索品質評価ガイドラインを作成しました.

まだ検索品質評価ガイドラインすべてを公表できるほど整ってはいないのですが,作成方法を一部公開します.

検索品質評価ガイドラインの作成方法

1. General Guidelinesを真似る

まずはGoogleの General Guidelines という検索品質評価ガイドラインを真似てざっくりガイドラインのテンプレートを作成しました.

2. 品質評価項目の作成

次に検索の品質を評価する項目を作成しました.
General Guidelines には品質評価項目が数十個ありますが,今回はほぼ踏襲はしませんでした.なぜなら,Googleとクラシルは検索の性質や検索の校正,評価する軸などが大きく異なるからです.

そこで,クラシルの検索に合わせて一から評価項目を作成し(ミニマムではありますが)今回は4つの項目を作成しました.
その中の一つを紹介すると「そのレシピが定番に近いか」という項目を作りました.
例えば(どちらも素敵なレシピですが),『[変わり種!ミニもずくハンバーグ]』と『[簡単デミグラスソースのハンバーグ]』だと後者のほうがより一般的に定番のハンバーグと感じると思いますし,定番さというのはレシピを選ぶ一つのポイントになると考えたからです.
レシピを選ぶポイントはつまりクラシルの検索結果で選ぶポイントにもつながってきます.

www.kurashiru.com

www.kurashiru.com

また,1つの項目につき5段階評価(-2,-1,-0,1,2点)をつけれるように設定しました. これで1検索キーワードにつき4項目*5段階をつけるので最低-8点〜最高8点の17段階の評価をつけれるようになりました.

3. 評価テストの実施

評価項目を作成し終えた後に,それで果たして数値がうまく付くのかをテストする必要があります.
今回は1人なので評価テストにかかる時間も考慮して評価する検索キーワードの標本調査を行い評価テストを実施しました. 評価テストを実施した結果,評価項目・評価方法ともに概ねよさそうだということを確認できました.

ミニマムではありますがこれで検索品質評価ガイドラインを作成することができました.

最後に

クラシルの検索品質評価ガイドラインを作成してみると難しいことだらけでした. しかし,本当に難しいのはこれを適切に運用しそれを検索の改善にいかすことだと思います.
検索品質評価自体取り組んでいるところは少ないかもしれませんがなにか知見を共有できれば幸いです.

delyではエンジニアを募集しています.よろしくお願いします.

www.wantedly.com

MLストリームパイプライン・プラットフォーム

こんにちは、機械学習エンジニアの辻です。

 

先日、SageMakerの活用事例で登壇させて頂きました。

machinelearningnighttokyo20181.splashthat.com

 

 

機械学習で一番時間の掛かる作業といえば、やはり前処理ですよね。データレイクからデータを取得して、必要に応じて様々なデータストアからデータをかき集めて、加工して、また別のデータストアに入れてと。。。

大量のデータをあっちにやったりこっちにやったりして、もう一体最初は何がしたかったのかさっぱりわからなくなってしまうことがあります。

 

 それに加えて、機械学習では、学習したモデルに対して推論しなければなりませんが、またこの推論結果が必ずしも決定的ではないという課題も含んでいます。

この、決定的ではないとはどういうことでしょうか?

動作が予測可能なもの、つまり、入力したものに対して、常に同じ経路で計算を行い、同じ結果を出力する、こういう振る舞いを決定的と 言います。なのですが、機械学習において、トレーニングで生成されるモデルというのは、その学習データによって、同じ責務を担ったモデルであっても振る舞いが変わってしまいます。

たとえば、同じ100万件のデータについて3クラス分類を行う場合に、昨日推論したときには、クラスAが33万件、クラスBが33万件、クラスCが33万件と綺麗に分類されていたとします。しかし、今日同じデータで推論したらクラスAが80万件、クラスBが15万件、クラスCが5万件と全然違う結果を出力したりして、まさに予測不可能なわけです。

 

MLストリームパイプライン・プラットフォームの構築

そこで、こんなストリームパイプライン・プラットフォームを構築しました。

f:id:long10:20181023180539j:plain



 

アーキテクチャーの変遷

実は、元々データレイクがBigQueryなので、構築の設計当初はGCP Cloud PubSubとDataflowを使って構築しようとしたんですね。親和性が高いし使いやすいので。

 

まさにこんな感じですよね。

  

しかし、アプリからCloud PubSubに対して、イベントをパブリッシュするためのSDKが提供されていなかったんです。RESTのエンドポイントは用意されてあるんですが、バッファリングやリトライについてはSDKに任せたいなぁという思いがありました。

 

 

 そして次に、AWS Batch、Step Functionsを検討しました。AWS Batchは非常に簡単に使えますし、大量のデータでも比較的高速に処理することが可能です。しかしワークロードが固定されてしまうことで柔軟なスケール変更が望めないことと、ワークロードの管理とコードの管理が別々になってしまうので、MLプラットフォームでのDevOpsが一元化できないという課題がありました。

 

https://beyondjapan.com/wp/wp-content/uploads/2017/04/Lambda_icon.png

 

Step Functionsは、私たちが思い描くイメージにもっとも近い形のワークフローを提供してくれるサービスではありましたが、AWS lambdaのみワークフローステップに指定することが可能ということで、やはりMLプラットフォームのDevOpsが一元管理できない課題を解消することはできませんでした。

「aws step functions icon」の画像検索結果 

しかし、これらのマネージドなサービスについては、必要に応じて今後もどんどん使っていきたいですし、クロスプラットフォームで提供される様々なサービスについても、良いサービスは適材適所で使っていきたい。そんな欲張りな思いもありました。

 

Kubernetesの採用

そんなわけでKubernetesを採用しました。KubernetesにはJobs、CronJobというのがありましてバッチを起動することができます。特にCronJobはスケジュール起動することが可能です。この機能を利用してストリームパイプラインを組もうと考えたわけです。こうすれば、もちろんクロスプラットフォームでも使えますし、決定的なワークロードのでもいけそうと判断したなら、GCP Cloud Dataflow、AWS Batch、Step FunctionsをCronJobから呼び出して使うといった恩恵も受けられます。現状でもコンテナ内から、AWS GlueのELTジョブに対してpySparkのスクリプトに投げて処理を移譲させたり、AWS Athenaのクエリに計算を任せたりと活用しています。

また、機械学習の非決定という課題ですが、推論結果のサイズによって次の処理で利用するインスタンスのサイズを変えたり、スケールアウトしたりと柔軟な対応が可能となります。

そしてもう一つ、Kubernetesを採用するに至った大きな理由がありました。

https://cdn-ak.f.st-hatena.com/images/fotolife/m/motobrew/20181018/20181018092821.png

Amazon SageMakerの活用

それが、Amazon SageMakerの活用です。

Amazon SageMakerは、機械学習モデルをあらゆる規模で、迅速かつ簡単に構築、トレーニング、デプロイできるようにする完全マネージド型プラットフォームです。SageMakerが提供するビルトインアルゴリズムと、DeepLearningフレームワークはすべて、コンテナでトレーニングJOBが起動できます。つまり、トレーニングJOBというコストの大きな処理をコンテナに移譲させることができるので、ワークロードのインスタンス自体は比較的小さくて済むということなんです。

 

 

f:id:long10:20181023180356p:plain

 

Kubernetesの運用

kubernetesの運用の中でいくつか工夫している点がありますので、少しご紹介したいと思います。 

 

argo

 

1つ目は、argoの導入です。argoはワークフローを細かくデザインできるだけでなく、直列処理、並列処理もYAML一つに記述することができるのでとても便利です。管理画面を立ち上げてワークフローが可視化できるのもいいですよね。

 

 Argo Image

 

kubewatch

 

2つ目はkubewatchの導入です。これにより、jobの開始と終了、エラーなどについては、こんな感じでkubewatchを通してicebergさんが知らせてくれてます。

 

f:id:long10:20181022181638p:plain

 

DataDog

 

そして3番目はDataDogの導入です。DataDogエージェントでPodの状況の監視を行っています。複数のnodeの状況を一元的に監視できるのでとても助かっています。

 

1

 

waterコマンドの開発

そして4つ目として、waterコマンドというのを開発しました。

kubernetesやminikubeなどコマンドがたくさんあって運用する上で把握するのが大変ですよね。argoの導入でargoコマンドまで増えてもう覚えきれません。そこでwaterコマンドで必要なコンテキスト単位にコマンドをまとめることで、コマンドのユーザビリティの向上に取り組んでいます。

 

installerでwater コマンドでインストールします。

f:id:long10:20181023093215j:plain

 

Usageが表示されます。

f:id:long10:20181023180457j:plain

 

デプロイコマンド

$ water deploy -e env -p project_name

こんな感じで新しいプロジェクトの作成からデプロイ、Podの編集などもwaterコマンドで一元的に統一的な操作が可能になっています。(全部ではないんですけど。。)

 

今後のロードマップ

現在、パイプラインの制御やPodのUp/Downについては、専用のPod(Control Room)を立てて制御を行ってるのですが、これをAmazon MQを使ってQueueによるイベントドリブンな設計を検討しています。 

 

Amazon MQ

 

必要に応じて学習エンジンも適材適所使っていきたいと考えています。

 

Amazon SageMaker

GCP AutoML

GCP ML Engine

 

最後に

 

delyでは今回紹介しましたMLストリームパイプラインを格好良くグロースしてくれるエンジニアを募集しています!

 

www.wantedly.com

辞書シノニム管理の運用

f:id:sakura818uuu:20181019092622p:plain

こんにちは.開発部のsakura ( @818uuu )です.
普段はレシピ動画サービス クラシルの検索を担当しています.

今回は辞書シノニム管理の運用について述べます.
一般的にもなかなか知見が共有されていない部分なので担当者の方は見て損はないと思います.
では,さっそく内容に入っていきます.

作業内容

辞書シノニム作業に必要な手順は主に2つです.

  1. キーワードの発見
  2. キーワードの登録

1.キーワードの発見

そもそも辞書シノニムがきちんと整ってないキーワードをどうやって発見するかという話があります.
自社では色々なポイントから見つけているのですがいくつかご紹介します.

ユーザーが検索したキーワードログから見つける

クラシルでは毎日ユーザーが検索したキーワードをチェックしています.
そのキーワードを検索回数が少ない順にソートして辞書シノニムがまだ未対応そうなキーワードを発見します.
なぜ検索回数が少ない順にソートするかというと,検索回数が多いものは既に対応済みであるからです.

キーワードごとの離脱率から見つける

これも1つ上の方法と似たものですが,キーワードごとの離脱率もチェックしています.それほど珍しい単語でもないのに離脱率が高いと辞書シノニムが未対応な可能性も高いのでここから見つけることもあります.

気づいた方から直接教えてもらう

社内の方やcsのフィードバックから教えていただき気づくこともあります.

本物の辞書を使う

この本物の辞書というのは検索の意味合いではない広辞苑といった一般の人が使う辞書のことです.
どうしても上記3つの方法だと検索後の対応になってしまいます.いままでに一度も検索されたことのないキーワードには対応できません.
そこで本物の辞書(食材辞典のようなもの)をあいうえお順に見て珍しそうなキーワードも登録している時期もありました.今ではコスパが悪いのでやっていません.

他にも色々な方法で辞書シノニム未対応のキーワードを見つけることが出来ます.

2. キーワードの登録

クラシルには自社で開発したMondoruというサービスがあります.

tech.dely.jp

Mondoruを使って該当キーワードを辞書・シノニムに登録しています.
登録した後は,テスト環境で一回検索することも必ず行っています.

更新頻度

クラシルではほぼ毎日辞書シノニムのメンテナンスを行っています.

作業時間

登録するキーワードの数に依存しますが,毎日平均して15分もかかっていないと思います.
辞書シノニムの運用を引き継いだ当初は4時間くらいやっていた時期もありました.

モチベーション

辞書シノニムを修正してきれいに形態素解析されて検索できた時はめちゃくちゃ気持ちいいです.
ここ半年辞書シノニムを担当していますが幸せです.

余談

検索が好きだったので入社する前に何百個かのキーワードでクラシルの検索を調査してメモしていました.
その時のデータも辞書シノニムの改善に大いに役立ちました.

f:id:sakura818uuu:20181018174114p:plain
入社前クラシル検索調査のメモ

気になっていること

自動化

辞書シノニムは一概に自動化できない部分があるのが難しいところです.
なぜなら辞書シノニムには意外にも専門的な知識が必要だったりするからです.
例えば私は辞書シノニムを担当しててはじめて「ふき味噌」=「ばっけ味噌」ということを知りました.こういうのをどうやって自動化するんだろう,と思っています.
辞書シノニムの自動化について知見をもっている方がいればぜひ教えていただきたいです.

Sudachi

Sudachiというanalyzerが気になっています.一般的にはanalysis-kuromojiを使っているところが多いと思うのですが(自社もそうです),Sudachiのスライドを見る限り高品質+高機能そうなのでとても興味があります.
Sudachi ❤︎ Elasticsearch - Speaker Deck

最後に

辞書シノニム管理をきちんと運用することで地道ではありますが確実に検索はよくなっていきます.
今回書いた内容がどなたかの参考になって検索がよくなっていけば嬉しいです.

delyではエンジニアを募集しています.よろしくお願いします.

www.wantedly.com

クラシルのデータ分析基盤

データ可視化推進室の深尾です。

delyではkurashiruリリース当初からデータ分析を重要視してきました。でも初めから使い勝手の良い分析基盤があった訳ではなく、これまでに何度も改良し今は4世代目となる分析基盤を中心に利用しています。今日はその分析基盤のお話です。まずこれまでの歴史から振り返ってみたいと思います。

 

クラシル分析基盤の歴史

第1世代:Google Analytics

f:id:motobrew:20181017150726p:plain

クラシルのリリース初期から導入していたのはGoogle Analyticsです。これは皆さんも使っていることが多いんじゃないかと思います。基本的には無料でログ収集・分析・可視化まですべてやってくれるので導入コストが低く便利です。

その一方でカスタマイズ性は低く、例えば独自の指標を取得してSQLで分析することは無料ではできません(有料サービスを使えばBigQueryにエクスポート可能)。また当初から、いずれA/Bテストを実施することは計画されていたので、そのための分析機能も必要でしたが、Google Analyticsはできることが限られます。そこでカスタムメトリクスを収集してSQLを実行するための第2世代の独自分析基盤を構築しました。

 

第2世代:fluentd + BigQuery + Redash

f:id:motobrew:20181017151517p:plain                    f:id:motobrew:20181017151628p:plain

モバイルアプリから行動ログを送り、それをfluentdで収集してBigQueryやS3に送ります(僕が入社して初めに取り組んだのがこれでした)。可視化にはRedashを使っています。この基盤は現在でもレガシー基盤として使い続けていますが、fluentdの運用コストがかかることや複雑なSQLを書ける人が限られてしまうことから、より使いやすい分析基盤を模索しており、そこで目をつけたのがFirebase Analyticsでした。

 

第3世代:Firebase Analytics

f:id:motobrew:20181017153025p:plain

ログの収集はFirebaseがやってくれますし、BigQueryにエクスポートする機能もついているので、第2世代の利点を活かしたまま、既存の課題も改善できる予定でした。ところが実際に使って問題となったのはカスタムで設定できるパラメータ数に上限があることでした。つまり独自の指標を分析するのにいずれ限界がくる可能性があります。またBigQueryにエクスポートした時にデータが特定のカラムに偏っており、クエリのスキャンコストが従前より高くなる懸念もあったため、第4世代のデータレイクへ切り替えることになりました。

 

第4世代:

Kinesis + Athena + Redshift

+ Metabase + 社内製ダッシュボード

f:id:motobrew:20181017145532p:plain

これが現在使っているデータ分析基盤です。構成図には載っていませんがログ収集基盤にはAmazon Kinesis Data FirehoseやAWS Glueを使っています。Sunnyと書いてあるのはSPAで作った社内製ダッシュボードです。

 

ログ収集基盤・データ集計基盤・ダッシュボードのそれぞれをリプレースしています。社内の人的リソースを確保できたことも背景にありますが、独自ダッシュボードを開発するに至った具体的な理由があります。

 

独自ダッシュボードの開発に至る経緯

構成図にもあるようにRedashに加えてMetabaseというオープンソースのダッシュボードも導入しています。当初はこのMetabaseだけを新ダッシュボードとして使う予定でした。

 

Redash & Metabase

Redashに対するMetabaseの利点は、動的にクエリを実行しやすいことが1つあるかと思います。例えばダッシュボードに2つの日付を入れるところがあって、O月O日からX月X日までのグラフを表示するといった使い方ができます。そしてRedshiftにデータマート(サマリーテーブル)を構築しておけば、簡単なSELECT文でデータを集計できるので、これまでより多くの人に使ってもらえるというのが開発前のぼくの考えでした。

しかし、実際にユーザーとなる各部署の社員にいろいろ話を聞いてみると、それまでの考えでは浅かったことに気がつきました。

 

クラシル用語で作られたダッシュボード

f:id:motobrew:20181018092622p:plain

データ分析基盤のユーザーが求めていたものはクエリが簡単に書けるダッシュボードではなく、クラシルの用語で構成された画面でした。例えばYahooの社内で使われているダッシュボードだったら「ヤフオク」とか「Yahoo!ニュース」といったメニューがあってそれぞれのメトリクスを見ることができるそうです。

 

クラシルのダッシュボードなら「検索キーワード」や「再生数」「すべてのレシピ」といったメニューがあれば、SQLを使わないフードコーディネーターやクライアント担当者でもデータを利用できますし、これまでSQLを使ってきたマーケターやエンジニアが分析にかける時間コストを短縮することもできます。

 

分析基盤のアーキテクチャ

データ分析基盤では、サービスがスケールした時でも集計クエリにかかる時間や費用を抑えられることが求められると思います。そして短期間で開発しなければならない事情もあったので3ヶ月以下で構築する方法を考えました。この辺りはエンジニアのスキルセットとの相性や、事業・組織のフェーズも影響があると思うので、どんなアーキテクチャやフレームワークで構築するのが一番良いということはないと思います。ぼくの場合はRedshiftを2014年ごろから使っておりメリットとデメリットもわかっていたので、これをうまく使ったり、弊社のDevOpsでワークフローサーバとして使ってきたRUNDECKをここでも使ったりすることで全体の開発工数を抑えることができたと思います。ここで社内製ダッシュボード周りのアーキテクチャを紹介します。

 

f:id:motobrew:20181018084341j:plain

(CoreUIのイメージ)

ダッシュボード

  • CoreUI (Vue.js) 製
  • Single Page Application
  • Kubernetes上のNginxコンテナで静的ファイルを配布
  • IP制限
  • OAuth2でGoogle認証

WebAPI

  • Express (Node.js)
  • RESTful API
  • RedshiftやElasticsearchに接続
  • IP制限

集計バッチ

  • Python製
  • AthenaとRedshiftでSQLを実行
  • クエリの結果を別のAthenaテーブルとして使うための独自フレームワークを実装(最近CTAS機能ができましたね^^)
  • RUNDECKサーバでスケジュール実行+ワークフロー管理

DWH

  • Amazon Athena + Redshift
  • Redshift SpectrumでAthenaのテーブルにアクセス
  • AWS Data Migration ServiceによりAuroraのDBと同期

インフラ、CI/CD

  • Kubernetes on EC2
  • AWS CodeBuild 

f:id:motobrew:20181018092821p:plain

今後のロードマップ

  • 画面追加
  • ワークフローのコード化
  • ユーザーごとの権限管理

こんな感じでいろいろあるんですが、もし新しい人に入ってもらえたら自分がやりたいところをやってもらったら良いんじゃないかなと個人的には思っています。データ可視化の面白いところは、SPA、UI/UXデザイン、集計バッチやワークフロー管理、Kubernetesなどを広く触れることがあると思います。もし興味があればぜひ一度オフィスに遊びにきてください。

 

delyでは現在エンジニアを大大大募集中ですよ!

 

 

 

State of dely devという取り組み

f:id:delyumemori:20180807164938j:plain

こんにちは。

普段はdelyのAndroidアプリの設計、実装とかその他もろもろを担当している梅森です。

最近弊社では、長期的な技術的な意思決定や技術的課題の可視化・解決の支援など、技術的な側面においてdelyの開発チームが成長していけるような取り組みを考えて実施しています。本稿ではそのうちの取り組みの一つをご紹介します。

エンジニアは普段の機能実装と並行して長期的なプロダクト改善を行うための施策を行っていたりしますが、アーキテクチャの移行やリファクタリング、自動テストといった施策は短期的には成果が見えにくいということもあり、業務的に近いエンジニアや成果が分かりやすいことをやっているエンジニアがどんなことをやっているかは分かるが、あまり仕事で関わらないエンジニアは何をやっているかよく分からないという状態になったり、会社としてはどのような技術的な意思決定を推奨しているかということが分からない(あるいはそもそも知見が存在しない)という状態になりがちです。

開発チームの規模が大きくなっていけば、例えばiOSやAndroid、サーバーサイド、SREといった技術領域ごとにチームを組織化してそこで知見を共有したり溜めたり、技術的な意思決定をしたりといったことを計画的に行っていくことになると思いますが、弊社くらいの規模ですとそこまでの取り組みを本格的に始めていくというのはまだまだ難しいものがあります。

そこで、手始めに各技術領域ごとで現在取り組んでいることや技術的な課題感、ロードマップ、最近興味のある技術領域などを定期的に開発チームにヒアリングして、Qiita:teamにまとめて共有するという取り組みを始めてみました。(delyは全社的にドキュメントをQiita:teamにまとめる文化に最近はなってます)

こんな感じです f:id:delyumemori:20180806160923p:plain

参考にしたのは、The State of Go 最新版へのリンク という1年に1回発表されるGo言語のそのタイミングでの状況をまとめたスライド(発表は実際に拝見したことは無いですが、いつか拝見してみたい)です。このような形で定期的に開発チームの技術的取り組みを概観した資料があったらいいんじゃないかということで、「State of dely dev」(なんでTheが抜けてるんだろう)というタイトルでQiita:teamに記事を投稿しています。

今回はなるべくそれを生に近い形でこちらのブログに転載します。箇条書きでざっくり書いてあるだけですが、雰囲気だけでも感じ取ってもらい、なにかの参考になれば幸いです。なお、この情報は2018年7月時点の情報なので、古い情報も多々含まれていることをご了承ください。


iOSチームの技術的課題

自動テスト

  • なかなかコード量増加のスピードに追い付けていない
  • 将来的にはテストもコーディングと一緒に書いていくスタイルにしたい

自動生成周り

  • サーバー側のswagger.json出力の準備が進んできた
  • JSON API形式の自動生成との相性は悪いが、後は手を動かすだけという状態
  • プロダクト開発とは違う課題が出てきたりして面白い、コーディング的な難しさに直面しがち
  • 使っているライブラリ自体の不具合も出やすい、一回ライブラリにプルリク送ってマージされたりといったことも
  • コミットメッセージを英語で書いたりして、英語への心理的ハードルが下がった
  • 人が少ないからこそ自動化的な方向性に投資していく

チーム開発について

  • 大きいプルリクの問題は解決しているわけではない、一日一回はWIPの状態でもコミット&プッシュして分割レビューの目安にするとかいう手はあるかも
  • 一個もプルリクきてないとSlackで通知を送ったりするのはいいかも
  • Dangerで500行以上のコミットの場合はWarningを出すようにしている

プロトタイプ開発

  • プロトタイプ専用のコードベースでプロトタイプ開発している
  • その中で既存コードを模写しつつリファクタできそうなところを発見したりしている
  • ビルドも速いのでiOSの新しい機能を試したりできる、今度そこで試した機能をリリースする(3Dタッチしたときにプレビュー出したり)
  • Codableでjsonパースするとかも試してみて発見がある
  • 検索画面のUIとかも新しい動きを試してみたりしている
  • アニメーションテストするときとかもいい
  • 将来的にはUIコンポーネントを分離して、それをプロトタイプ開発でブラッシュアップして本体に取り込むみたいなこともしたい
  • ペーパープロトタイピングで認識をすり合わせて作り始められているのでスピード感をもって作れている

ライブラリの更新問題

  • ライブラリが更新されたら自動的にプルリクを作るものを作ったが、なかなかうまくいっていない

Androidチームの技術的課題

自動テストについて

  • Kotlinはテストコードから導入する予定
  • まずはUIテストからやる予定
  • テストコードは気兼ねなく捨てられるのでまずは捨ててもいいからコードを書いていく

Redirectable UIについて

  • ConstraintLayout 1.0にはすっかり慣れた
  • ConstraintLayout 2.0 - MotionLayoutの可能性

アクセシビリティ対応をそろそろ考えたい

  • 文字サイズ大きくしても破綻しないUI
  • 画像にキャプションつけるとか(画面読み上げ対応)
  • 色覚対応
  • 長期的に対応したい
  • とりあえず文字サイズが一番大きい

JetPack対応

  • 今のところすぐ導入すべきものはないが
  • これは~という理由で導入してないと言えるくらいのレベルには理解しておきたい

ビルド遅い問題

  • Firebase、Google Play Servicesのアップデート等でだいぶビルドが遅くなってしまった
  • 都度都度ライブラリの依存関係の見直し等を行うことでビルド遅い問題に対処していきたい
  • マルチモジュール化の検討もすすめる

Webフロントエンドチームの技術的課題

もろもろ進行してます

  • Webpackerへの移行は完了
  • jQuery依存からの脱却は完了
  • SSR導入は決定、作業中

PWA

  • コンパイルされたJSを分割してそのページにあったものをロードするということができるようになった
  • webpackで任意のチャンクに分割してJSを別々にロードすることのできる機能を活用している

SPA

  • レスポンシブ表示で同じ構造で表示するのではなく、PCとSPに最適化された構造に移行していきたい
  • ローディング時に読み込み対象のプレースホルダーを導入したい、今は読み込みでガタガタ動いてしまう
  • デプロイ、テスト遅い問題(デプロイ40分)

バックエンドチームの技術的課題

既存の技術的負債の返済

  • まずはRails 5.0へのアップデート
  • 現状のアーキテクチャでもクラシルのリード過多なアプリであればスケールしていけそう

A/Bテスト

  • API側のA/Bテストも出来るようにする予定

テスト遅い問題

  • テストを早くする方法は分かっているが作業はめんどくさい、ElasticSearch、キャッシュも分離しないと早くできない
  • 実際にどのテストケースが遅いかを調べたりとか
  • データベースのボトルネックになっているクエリを潰しているが、先回りして検出してやりたい

Swagger

  • swagger.jsonの自動生成については目途は立ったのでマイルストーンを立てる
  • iOSとの連携も進行中

API定義をyamlで記述するトレンド

  • APIをyamlで記述するのが流行っている
  • Googleでyamlで書いた構造にAPIに沿っているかテストできるサービスを出したりしている
  • API構造をyamlで記述しているのがメインストリームになりそう
  • その先のアクション(APIを丸ごと自動生成したり)がありそう

機械学習チームの技術的課題

レシピデータの構造化を進める

  • レシピ情報の構造化を進めている
  • レシピの特徴量を出していくにあたって、レシピを入力する側の手順を整理したい。レシピ情報の構造化を進められれば、料理の難易度を判断したりもできそう
  • ルールベースのバリデーションでチェックするなどの方法であれば、そこまでコストがかからなく役にも立ちそう

SREチームの技術的課題、ロードマップ

データ可視化プロジェクト

  • 独自のダッシュボードを作っている
  • Vue.js+Express
  • メインのデータレイクはエターナルポース
  • SQL書かない人が使うようなダッシュボードがほしい
既存のプロダクトについて
  • Metabase、Re:dashなどはSQL書く人用のプロダクト
  • サービスの用語を使ってデータを分析できるようなツールがない
  • グラフについてもプリセットのものでなく、より見やすいものが欲しい
  • 変数についても細かい制御ができない
プロトタイプ開発
  • まずプロトタイプを作って、今クオーター中にプロトタイプをもとに機能追加の土台になるものをリリースし、アジャイルみたいな開発フローに載せたい
  • 一チームいてもいいくらいの規模
  • 1年後を考えた時に、自分たちのサービスの用語で整理された可視化ツールがあった方がいい
  • 現状はレポーティング、データ分析をバラバラ、属人的にやっている、それを統合したい

データレイクの移行(エターナルポース)

  • できた、追加でやることはない
  • 必要になったら過去データ(Firebase, Logpose)の移行もやる

こまごまサーバーインフラ整理プロジェクト

  • こまごまとしている社内システム的なプロジェクトがあり、メンテされていない
  • それらのデプロイがうまくいかなくなっている、方法を統一したい
  • どういうものがあってどのような構成になっているかは把握できている

検索周りの技術的課題

検索アルゴリズム自体の改善

  • バックエンドのロジックのA/Bテストをやるかもしれないので、それが入ったらロジック部分の検証をやるかも

検索ログの分析の知見を開発部以外でも活用する

  • 検索のログデータを活用して、いろいろなところにデータを渡せるようになってきた
  • 日ごとの検索ランキングをSlackで自動で共有したり
  • 検索共有MTGで検索トレンドを共有したりしている、最近はタイアップやマーケの人にも共有している
  • 検索自体の改善もそうだけど、レシピ選定もデータに基づいて行ったりできるようになってきた
  • 調理部の作業フロー自体の改善にも貢献できるようになってきた
  • 最近では、2か月先向けのレシピの提案を行えるようになってきた

検索内容の改善

  • 検索されても存在しないクエリがたくさんあったのが、最近はいい感じに埋まってきた

ちょっと長くなってしまいましたが、delyの開発チームでは最近このような取り組みをしていますというご紹介でした。

delyでは、会社の開発文化自体の発展、促進に興味がある人を募集しています。

(あとAndroidエンジニアも募集してます。)

クラシル、不屈のキャッシュ戦略

こんにちは!

プロダクトマネージャーをしている奥原 (@okutaku0507) です。前までサーバーサイドのリードエンジニアをしていました。

delyの開発ブログが長らく更新されておらず、不甲斐ないです。これからは活発にdelyが取り入れている最新技術や実際にあった事例、取り入れているアーキテクチャなどを中心に発信していきたいと思っています。 

久しぶりの今回は、delyが運営/開発しているレシピ動画サービスであるkurashiruの涙あり、笑いありのキャッシュ戦略について歴史と実際の事例を元に書いていきたいと考えています。最後には、僕が作成したクラシルに用いられているキャッシュ戦略をgemにしたライブラリを紹介いたします。

はじめに

サーバーサイドチームはクラシルの開発から1年半程度まで、主に僕一人しかいませんでした。TVCMによる急激なユーザー数増加や新機能開発社員100人を支える管理サイト開発などのサーバーサイド面を一人で担当せざるを得ませんでした。言い訳になってしまうかもしれないですが、最小工数で最大限いい感じにする必要があり、キャッシュ周りに十分な時間が割けず、他のエンジニアの方々から見れば継ぎ接ぎで不恰好な印象を受けるかも知れません。リソースが恐ろしく枯渇しているスタートアップの一エンジニアの苦悩と格闘の歴史だと思っていただければと思います。もしよければ、建設的な意見をいただくかあるいは仲間に加わっていただき、一緒に改善していければ幸いです。

そして、この記事はこんな方におすすめです。もちろん、キャッシュ戦略自体はどの言語で記述されていても役立つと思います。

  • ユーザー側でDBへのリクエストがほぼリード
  • 同じ画面のリクエストが多く、キャッシュ効率が良い
  • サーバーサイドリソースが恒常的に枯渇状態

 

1. 全リクエストがDBにアクセス時代

ELBなどは省き、必要最低限の構成を描くとこんな感じでした。

 

f:id:okutaku:20180721184937j:plain

 

Railsアプリケーションが一つのデータベースを参照するという、とてもシンプルな構成です。この構成で問題なのが、全てのリクエストでデータベースへ問い合わせを行なっている点です。たとえ同じクエリが走って同じ結果が得られたとしても、全て問い合わせが行われているため、とても効率が悪いです。 

そして、サービスがグロースしていく中で一つの問題が起きはじめました。 

プッシュ打つとサーバー落ちる不可避問題。

全端末をターゲットにプッシュを打つとサーバーが落ちるという問題に行き着きました。その原因として、様々な原因が考えられられ、メモリやCPUなど様々なメトリクスを調べましたが、わからず仕舞いでした。

f:id:okutaku:20180721213947p:plain

f:id:okutaku:20180721214019p:plain

f:id:okutaku:20180721214114p:plain

そんなこんなあり、夜も眠られない日々が続きました...

※ 「ゆうすこ」は弊社代表です 

 

そして、DBがボトルネックになって結果的にレイテンシが上がり、LBのヘルスチェックが落ちているのではないかという考えに至り、キャッシュを導入することになりました。

f:id:okutaku:20180721214736p:plain

何はともあれ、CTOが徹夜でコードを書くという涙ぐましい努力により、僕らはいっときの平和を手にしました。

 

後にSREがジョインし、調べるとリソースが効率的に使われてなかったことも原因だとわかりました。

tech.dely.jp

 

2. オーソドックスなキャッシュ時代

AWSのマネージドサービスであるElastiCacheを導入しました。

クライアントに返すAPIレスポンス(json)を、パラメータを含めたエンドポイント毎にElastiCacheにキャッシュするようにしました。そうすることで、DBへ過度に不要な問い合わせが発生しなくなりました。簡単ですが、構成は以下のようになります。

 

f:id:okutaku:20180721231346j:plain

 

これによりしばらくは枕を高くして寝ることができました。

しかし、現実はそんなに甘くはありません。

弊社ではインシデント級のアラートはイワさんが教えてくれます。これはレイテンシが1秒を超えてるよという恐怖の通知です。この通知がプッシュの時に頻繁にくるようになりました。

f:id:okutaku:20180721232528p:plain

このインシデントについて調査していくと、ElastiCacheのGetHitsというメトリクスが引っかかりました。

f:id:okutaku:20180721233211p:plain

当時はElastiCacheを3台立てていたのですが、この図からわかるように、明らかに1つのサーバーにリクエストが偏っています。また、この時間のRailsのログを調査すると以下のようなエラーが出ていることがわかりました。

W, [2016-08-13T12:13:26.663212 #6100] WARN -- : localhost:11211 failed (count: 0) Timeout::Error: IO timeout
D, [2016-08-13T12:13:27.053149 #6100] DEBUG -- : #<Dalli::NetworkError: Socket operation failed, retrying...>
D, [2016-08-13T12:13:27.553223 #6100] DEBUG -- : Dalli::Server#connect localhost:11211
D, [2016-08-13T12:13:28.311837 #6111] DEBUG -- : #<Dalli::NetworkError: Socket operation failed, retrying...>

f:id:okutaku:20180721234031p:plain

当初、一つのリクエストに対して一意にキャッシュを取得するためのキーが決まるようになっていました。プッシュでホームを開かせるように、特定のリクエストにアクセスが集中する場合、TCPポートの上限に達してポートが足りなくなってしまいます。そうすると、Rails側でキャッシュが上手く取得できず、Railsがタイムアウトするまで待ってしまうため、結果的にレイテンシが上がっていたということになります。

この解決策は単純で、集中してしまうなら分散させればいいと当時は考えました。

cache_key = “test-key”
cacher = Cacher::ObjectCacher.new(key: cache_key)
cacher.write(“test-str”)
# Cache write: test-key-1
# Cache write: test-key-2
# Cache write: test-key-3

上記のようにキャッシュキーの後ろに数字をつけるようにすることで、同じ内容を分散させることができます。厳密に言えば、クラシルで使っているdalliというgemのロジックによるため、均等に分散させることはできませんが、目的は果たすことができました。

こうして、再び弊社に平和が訪れました。

f:id:okutaku:20180721235033p:plain

※ 現在のメトリクスをスクショしたため上と異なります

 

3. キャッシュ分散時代

キャッシュキーの後ろに数字をつけることで、いい感じにキャッシュが分散され、イワさんから怒られることは劇的に減りました。

しかし、まだまだ現実は甘くありませんでした。

たまーにですが、プッシュ時にイワさんから怒られることがありました。

f:id:okutaku:20180721232528p:plain

詳しく調査をすると、どうやらイワさんから怒られる時は、ElastiCacheのキャッシュミスが急激に増加していることがわかりました。つまり、キャッシュ切れです。分散させたのはいいですが、全て同じ有効期限を設定していたが故に、切れる時は分散された全てのキャッシュが切れていたということです。

ならば、有効期限を長くすればいいという話になりますが、

  • タイアップ広告が指定された時間に公開されること
  • キャッシュを長くしてデータの不整合がおきること

などの理由によりそれは難しい状況でした。
また、設計はシンプルに保ちたかったので、ユーザーから来るリクエストのエコシステムの中でいい感じにやってくれることを目指した方が賢明です。

そこで、ヒラメキました!

先ほどの分散されたキャッシュキーに対して、有効期限のグラデーションをつければいいのではないか。つまり、図で書くとこのような感じです。

f:id:okutaku:20180722120709p:plain

この図において、key-1~3は同じ内容をキャッシュしており、キャッシュの有効期限だけが異なっています。この場合、key-1の有効期限が最も短いため、最初にキャッシュ切れを生じますが、有効期限がそれよりも長いkey-2~3は生き残っているので、このキーを参照しているリクエストはDBへの問い合わせを行いません。

また、矢印の色の違いはキャッシュ内容の世代を表しています。例えば、key-1がキャッシュ切れになり、新たにキャッシュの内容を更新しようとした際に、他のkey-2~3も同時に上書きを行います。こうすることで、あと少しでキャッシュ切れになろうとしていたkey-2~3が新たに有効期限が設定されます。この仕組みを繰り返すことで、実質的にキャッシュミスが生じるのは有効期限が最も短いkey-1になり、キャッシュミスになる確率がこのケースでは1/3になります。

cache = ActiveSupport::Cache::DalliStore.new
cache.write('millas', 'cake', expires_in: 1.minutes)
# Cache write: millas-1 ({:expires_in=>60 seconds})
# Cache write: millas-2 ({:expires_in=>120 seconds})
# Cache write: millas-3 ({:expires_in=>180 seconds})

これで、弊社に恒久の平和が訪れました。

エンジニアはプッシュの度に怯えた日々を過ごすことなく、自分がリソースを割くべきことに集中することができるようになりました。

ここまでのストーリーをQiitaにも書きましたので、リンクを貼っておきます。また、この仕組みをRailsに簡単に導入することができるgemも作ったので、Railsエンジニアのリソースが枯渇していて、クラシルのようにリードが激しいメディアの方、参考にしてもらえれば幸いです。

qiita.com

github.com

 

追記

このキャッシュ切れの問題をThundering Herd問題と言うようです。

Kazuho@Cybozu Labs: キャッシュシステムの Thundering Herd 問題

 

4. キャッシュ分散グラデーション時代

「ついに平和が訪れた」と誰しもが確信していました。

しかし、平和というものはそんなに永くは続きませんでした。

上記の対応をした後でも、ごくごく稀にプッシュ時にイワさんから怒られることがありました。まだ敵いたの!?という感じの少年ジャンプみたいな世界観。

f:id:okutaku:20180721232528p:plain

※ ちなみに、イワさんはルフィの心強い味方です

 

原因を精査すると、理由は同様でキャッシュ切れのようです。よくよく考えると、有効期限をグラデーションしたとしても、一つのキャッシュに割り当てられるリクエストは1/nになります。つまり、図で書くとこういうことです。この図で注目して欲しいのは、1つのキャッシュキーに割り当てられるリクエストの割合が、3割強ということです。

f:id:okutaku:20180723133132p:plain

例えば、プッシュ時に10,000のリクエストが来た際にキャッシュ切れが生じると、約3,000リクエストがElastiCacheをすり抜けてDBへ問い合わせることになります。これはまずいぞ、ということになります。

ここでもヒラメキました!

このリクエストを各キャッシュキーに割り当てる配分にもグラデーションをかければいいのではないかと考えました。キャッシュの有効期限が短いキャッシュキーほど少ないリクエストを受け取るよう、いい感じのロジックを作ってリクエスト数にグラデーションをかけることにしました。図にすることこのような感じになります。

f:id:okutaku:20180722140756p:plain

この図が意味するところは、キャッシュキーの後ろにつける数字 (magic number) が小さいほど、頻度 (リクエストが割り当て割れる配分) が低くなるということです。上の図と照らし合わせると、有効期限が最も短いキャッシュキーに割り当てられるリクエストの割合が一番小さいということを意味します。つまり、キャッシュ切れが生じるリクエストの割合が数%になることで、ほとんどのリクエストがDBに問い合わせにいくことがなくなりました。

これを導入してから、キャッシュ切れが生じることは全く無くなりました。

本当に本当の平和が弊社を包み込みました。

 

終わりに

delyでは、技術を使って世の中に幸せを届けたいと一緒に戦ってくれるエンジニアを強く募集しています。どのポジションも貴重なリソースが限られているベンチャーでは、やりたいことが山のようにある中で何に注力するか、そしてそれを如何にして最速でユーザーに届けられるかが重要になります。その中でも自分の強みを活かして頭がちぎれるまで考えて実装することが僕らエンジニアの見せ所だと思っています。

 

一方で、新しいソリューションも積極的に取り入れ、本番環境に適応しています。その取り組みの一つが評価され、AWS Summit Tokyo 2018で弊社CTOが基調講演させていただいた動画を貼っておきます。


Day3 基調講演(日本語同時通訳) | AWS Summit Tokyo 2018

 

僕らはサービスを伸ばしつつ、エンジニア個人が技術的に挑戦したいことに、どんどんチャレンジして、サービスとともに成長していきたいと強く思っています。エンジニアに限らず、そのようなマインドをお持ちの方と一緒に仕事ができる日を心待ちにしております。是非一度お茶でもさせてください。 

 

 デザインブログも始めました。

 

kurashiruの検索UX改善プロジェクト

こんにちは。
delyでISE(In-house System Engineer)をやっている@_skuwaです。
kurashiru[クラシル]のグロース、プロダクト改善の為の基盤の設計・開発を行っています。

今日はユーザーの検索行動のUXを向上させるために立ち上がった、検索改善プロジェクトについて書こうと思います。

検索機能における、UX上の課題点

  • 検索したものの、レシピ数が少ない
  • 豚肉、パスタなどの曖昧なワードでの検索時に結果が多すぎる(ユーザーが求めているレシピの割合が低くなってしまう)
  • 明らかに不適切なレシピが検索結果に混ざり込んでしまっている

その結果、検索後の離脱率(動画再生せずに直帰した割合)が高い状態になっていました。

課題解決へのアプローチ

delyでは基本的にグロースは開発チームが行っています。
しかし、今回の検索はドメインの専門知識があったほうが質の良い仮説が立てられるということで、調理部などの各部署と連携したプロジェクトが立ち上がりました。

やったこと

  • 部署横断での検索改善プロジェクトチームの立ち上げ
  • 料理人の方々がPDCAを回せるように、SQLを書かなくても主要なデータ分析を行えるWebアプリの作成(Mondoru)
  • Mondoru上でElasticsearchの辞書、シノニム管理
  • 数値やKPIのSlack通知
  • 検索数が多いのに結果が少ないキーワードを抽出し、レシピを開発

Mondoru

delyではリポジトリ名やアプリケーション名をワンピースのキャラにするという習慣があるので、今回はそれっぽい『シャーロット・モンドール』から名前を付けました。

Mondoruには主に以下の機能があります。

  • 辞書、シノニムの追加
  • スコアリングの調整
  • 上記チューニングでの検索結果をテストする
  • Elasitcsearchの管理
  • 検索離脱率などの主要データを画面をポチポチして集計、出力する
  • データのビジュアライズ
  • その日の検索データ速報をSlackへ通知

改善でやったこと、成果

基本的には季節ごとの検索傾向や離脱率を見ながら辞書とシノニムを追加していくことになります。
調整した検索ワードの離脱率と動画再生画面への遷移数の変化を見ながら、ベストな検索結果を探っていきます。

f:id:skuwa:20170912210722p:plain

日々の検索データを眺めながらノイズの割合が多いものに関しては辞書とシノニムを活用したチューニングを、検索結果数が少ないものは該当キーワード向けのレシピ開発を行います。

↓検索結果改善によって、検索後に動画再生される割合が急速に増えてきています。 f:id:skuwa:20170912211633p:plain

今後の課題

レシピ開発によってニーズのある検索結果を充実させ、辞書やシノニムで検索結果に含まれるノイズを減らすことに成功しました。

ただ、冒頭に述べたように検索結果が多すぎる時にユーザーに刺さるレシピの割合が少ないという問題は解決できていません。データで見ても、具体的な料理名での離脱率は減少していますが一般的な食材名での離脱率はあまり変化が見られません。

そこで、検索関連のユーザー行動データを元にしたサジェスト機能を実装中です。

サジェストには2種類あって、絞り込みのサジェストと置き換えのサジェストがあります。

絞り込みサジェスト

『パスタ』を入力したユーザーに対して、

  • 『パスタ トマトソース』
  • 『パスタ 魚介』

をサジェストする。

一般的なサジェスト機能です。
キーワードの検索数や、検索結果の件数などを元にスコアリングして表示を行います。

置き換えサジェスト

『豚肉』を入力したユーザーに対して、

  • 『豚こま肉』
  • 『豚ひき肉』

をサジェストする。

『豚肉』といったカテゴリに近い検索ワードは、サジェストでより細かいカテゴリに置き換えすることで検索結果を最適化できるようにします。

冷蔵庫に『豚こま肉』が入っているユーザーが『豚肉』で検索しようとした際に、『豚こま肉』をサジェストすることで、より最適な検索結果にユーザーを誘導することが可能です。

まとめ

ドメインの専門知識を活かすグロース基盤を作ることで質の良い仮説を立てることができ、『超速』でのグロースをすることができました。

delyではその他にもグロースの質と速度を爆速にする基盤やアプリケーションの開発に投資をしており、Mondoruのようなアプリケーションの開発を多数行っています。

世界最速のグロースに挑戦したいという方、是非応募してみてください!

www.wantedly.com