クラシル開発ブログ

クラシル開発ブログ

クラシルAndroidプロジェクトの開発者体験の向上を頑張ってます!2

f:id:meilcli:20200827161534p:plain

クラシルを開発してるAndroidエンジニアのMeilCliです。前回のクラシルAndroidプロジェクトの開発者体験の向上を頑張ってます!を投稿してから進捗があったので報告します

前回予告した内容は以下の感じでですが、設定した目標通りに行動できないのがエンジニアです。ご了承ください

  • 長年放置し続けてきたlintの対応、不要になったリソースの削除
  • 使用しているOSSなどのライセンス表記のための集計ツールをMeilCli/Librarianに置き換える
  • ライブラリーアップデートの自動検出・通知
  • モジュールのbuild.gradleの共通な宣言部分をbuildSrcへまとめる

長年放置され続けてきたlint問題

不要なリソースの削除

クラシルの開発は多種多様な機能を作っては検証し、価値がなければ消すの繰り返しをしてきました。そのため開発者が消し忘れているリソースがたくさんあり、そのリソースによってビルドに時間がかかっているのではと考え大掃除することにしました

f:id:meilcli:20200806192516p:plain
大掃除の様子

大掃除自体はIntelliJ IDEAやAndroid Studioに搭載されているInspect Code機能(ツールバーのAnalyze=>Inspect Codeで使えます)を使えば機械的に使用していないコードなどをあぶり出してくれるので比較的楽に行うことができます
※Inspect Codeでの機械的なチェックだと検知ミスやリフレクションなどのすり抜けがありえるのでプロジェクトの全文検索などを駆使して人力で消すかどうかの最終確認をすることをお勧めします

結果としてはビルド時間は大して変わりませんでした(残念)
ただ、lintのwarningを結構な数減らすことができたのとダウンロード時のapk容量を2MBほど減らすことができたのでやって正解だったかなと思います

今後の計画

不要なリソースを削除したあとではAndroid lintのみで約400件ほどのWarningがある状態となりました(IntelliJ IDEAのInspect Codeベースだとそれを遥かに超えるWarningが貯まっています)

それらのWarningを今すぐに撲滅するのは現実的ではないため、計画を立てて減らしていき最終的に撲滅するということにしました

f:id:meilcli:20200806193148p:plain
lint戦略

画像はAndroidチームに共有したlint戦略です。概要としては以下の感じです

  • ファーストステップ
    • 2Q終わり(つまり9月末)までを想定
    • ルール調整や重要度の高いものへの対応
    • モニタリング環境整備
    • セカンドステップの準備(PullRequestでのwarningのインラインコメント)
  • セカンドステップ
    • 撲滅作業が苦にならない程度に減るまでを想定
    • Warningを増やさない・気づいたら消す・気が向いても消す
  • ファイナルステップ
    • Warningをひたすら消す

計画は立てたのであとは実行するのみです。モニタリングは週1ペースでWarningの総数や推移が分かればいいかなと考えています

Android lintとDetektに関してはGitHub ActionでWarningの計測と統計・推移画像を生成するMeilCli/android-lint-statisticsMeilCli/detekt-statisticsを作成&導入をし、現在は週1で試験実行しています

他にもIntelliJ IDEAのInspect CodeをCI上で実行したいところなのでJetBrains/inspection-pluginを使っていこうと考えています*1

Librarianの導入

クラシルで使用しているライブラリーのライセンス表記を自動生成するツールとして自作のMeilCli/Librarianを導入しました

今までも自動生成ツールを使用していたのですがいくつかの問題を抱えていました

  • マルチモジュールプロジェクトによる複雑なConfigurationによるライブラリーの誤検知
  • 新たなライブラリーを導入したなどにConfigファイルを手作業による調整
  • Maven Artifactごとにライセンス表記が生成されていたことによって膨大なライセンス表記
  • HTMLを生成していたため2MBにもおよぶファイルをアプリに組み込んでいた

これらの問題が表面化し始めた頃、ちょうどMeilCliが他のライブラリーを作るためにライブラリーのライセンス集計ライブラリーを作っていたためそれを導入する運びになりました*2

実際に導入して数週間経ちましたが検知精度としては期待値通りになっています。すでにLibrarianを導入したものがストア配信されているので気になる方はぜひクラシルをインストールしてマイページの設定からライセンス表記画面を見てください

また、個人ブログでLibrarianの解説記事を書いているので気になる方はぜひ見てください

ライブラリーアップデートの自動検知・通知(+ ついでにbuild.gradle整理)

さて、みなさんdependabotという便利なbotをご存知でしょうか?リポジトリー内のライブラリー依存宣言部分をスキャンしてアップデートがあったらバージョン宣言を置き換えたPullRequestを自動で作ってくれるbotです

(npmやnuget方面の方は)セキュリティー関連の警告やPullRequestが自動で作成されるのを見たことがある人がいるかもしれませんが、dependabotがGitHubに統合された結果、リポジトリーにdependabotの設定ファイルを追加すればセキュリティー関連のPullRequest以外も自動で作成されるようになりました*3

PrivateリポジトリーでもGitHubへのデータ提供を許可していればdependabotを運用することができるのでこれを使わない手はないですよね?ということでクラシルAndroidにもdependabotを導入しました

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "gradle"
    directory: "/"
    schedule:
      interval: "weekly"
    assignees:
      - "MeilCli"
    labels:
      - "dependencies"
    commit-message:
      prefix: "[improvement] "
    open-pull-requests-limit: 15

今の所、クラシルではこのような設定ファイルで運用していこうと考えていて、運用を始めて数週間経ちましたが毎週何個かアップデートがあるという状態になっています*4。この設定ファイルで毎週画像のようなPullRequestが作成されています

f:id:meilcli:20200811191001p:plain
dependabotが作成してくれたPullRequest

また、Gradleプロジェクトでdependabotを導入するには多少手心加えるとより良いマルチモジュールプロジェクト運用ができるのですが、ここで解説すると長くなるためマルチモジュールプロジェクトで導入したい方は以前に個人ブログで書いたdependabot導入記事を参照ください

あと、ついでに行ったbuild.gradleの整理ですが、具体的に言うとbuild.gradleで記述する内容をbuildSrc配下のGradle Pluginとして記述するようにしました。この記事に書くとコードで埋め尽くされてしまいますので、同様な記述をしているLibrarianを見ていただければと思います。↓コードリンク

Librarian/buildSrc/src/main/kotlin/net/meilcli/librarian/gradle/plugins at master · MeilCli/Librarian · GitHub

その他

今後について

細かい改善はいろいろとありますが、皆さんにご紹介するほどの内容になると

  • Android lintのインラインコメント対応
  • inspection-pluginの続報
  • ライブラリーアップデート時の差分検知

といったところを次回予告としておきます


join-us.dely.jp

新しい採用ページできたらしいです

*1:Gradle v6に対応できていませんでしたがそこはコントリビュートしておきました、最新版を使えばおそらく皆さんのAndroidプロジェクトでも動作するはずです

*2:ライブラリーを作るためにライブラリーを作る、GitHub Actionを作るためにGitHub Actionを作る、それがMeilCliです

*3:個人的にはいつの間に統合されたんだという感じです

*4:dependabot任せにできないPullRequestは手作業でアップデート作業したり、場合によっては何らかの都合によってPendingするといったこともやっています

Android チーム全員で「ぽちぽち」してクラッシュや不具合を防ぐ!

f:id:rnitame:20200814180012p:plain

こんにちは、クラシル Android のプロダクトマネージャーをしている tummy です。

先月から立て続けに Android チームで行っている取り組みについて紹介させていただいてますが、今回も Android チーム全員で毎朝アプリを触る時間、通称ぽちぽち会について紹介させていただきます。

クラシル Android アプリの開発上の課題

Pixel シリーズが日本に来てから、道端を歩いていても以前に比べると Android ユーザーを見かけるようになりました(筆者の観測内では)。しかし、日本ではまだまだ iOS ユーザーのほうが比率としては高く、dely 社内もその傾向にあります。

弊社にはプロダクトレビューという毎週定期的に時間を取ってアプリをいじる文化が存在しますが、iOS チームに比べて人が触る機会が少なかったり、iOS で良かった施策を Android でも行う際などにこのデザインは Android 向けのものなのか?といった定性的な評価をする機会が少ない状態にありました。

クラシル Android チームの開発体制の課題

また、弊社の Android チームの体制にもいくつか課題がありました。

  • Android チーム以外がリリース前の機能に対してフィードバックできる機会が、専用のチャンネルでアナウンスされたタイミングからリリースされるタイミングまでの間しかない
  • Android を普段使いしている人がクラシルの Android のデバッグ版を触る機会があまりなく、Android ユーザー視点でのプロダクトへの指摘がやりにくい

以上のような課題を、Android チームでそのアプリをいじり倒して確認することや気になったことを逐一共有し合う時間を設けることで払拭できないかと考えました。これをきっかけに、ぽちぽち会が誕生します。

sandbox アプリとぽちぽち会の誕生

sandbox アプリについて

Google Play から落とすことができるアプリと共存でき、常に開発ブランチの最新バージョンが反映され、API エンドポイント等も基本本番に向いているアプリです。日次でビルドされます。

アプリを触る人の手間を最小限にするために、一度 DeployGate 経由でアプリをインストールすれば、 適宜日次で勝手にアップデートが降ってきてアプリを触ることができる状態になります。 API サーバーに修正が反映されておらず確認できない、ということも考えられますが、 なるべくいつも通りの環境でアプリを触れる状態にして置くことを意識しています。 普段と状況が違っているというのはユーザーとしてアプリを触っているという気持ちから離れてしまう原因になりがちだからです。

ぽちぽち会について

f:id:rnitame:20200717155414p:plain

朝に 1 時間とって Android チームで集まってアプリを触る時間を毎日設けました。専用のタグがついているプルリクがリストアップされているので、そのリストを 1 つ 1 つ確認します。 プルリクを 1 回は sandbox ブランチに対してマージして確認し、OK が出るまでマージしてはいけないというルールにしています。 Android 的におかしいデザイン(たとえばマージンが狭くて気になる、もう少しフォントサイズが大きいほうが良いんじゃないかなど)や要件的に他の実現方法のほうが良いのでは?といった議論がここでなされます。

f:id:rnitame:20200714085233p:plain

文言のやりとり 実際リリースしたもの
f:id:rnitame:20200714085535p:plain f:id:rnitame:20200714085616p:plain

上記の例では、コーチマークに表示する文言が微妙に気になる、ということで少し調整したりしました。実際にここで決まった文言が最新アプリで表示されるようになっています。

文言のやりとり 実際リリースしたもの
f:id:rnitame:20200721173234p:plain f:id:rnitame:20200721173812p:plain

また別の例では、ダイアログの消えるタイミング・消すことができるタイミングについて議論し、なるべくユーザーが不快に思わないような挙動に寄せる決定をしリリースに至ったこともあります。

3 ヶ月ほどやってみて

チームメンバーに聞いてみました

  • 作業しているエンジニアが考慮しきれなかった箇所を他の人が発見してくれたり、作業として未完成な段階でもUXや仕様上のフィードバックを貰えるので作業の手戻りを抑制するという点でよかった
  • 毎朝 30〜60 分ほど時間を費やしてるので時間を浪費しすぎてないか?という不安はある。ユーザーさんに早く機能を届ける量という視点と正しく機能を届ける質という視点での葛藤
  • 毎朝コミュニケーションを取ることで、実装の方向性をチーム内で合意を取りつつ収束させることができ、温度感のすり合わせができたのでよかった

確かに、ぽちぽち会が終わったらもうお昼になっているという日もあったりするので、タイムマネジメントは考えていく必要があるなぁと思いました。 まだまだ改善の余地はあると思うので、引き続きやっていきます 💪

最後に

dely ではエンジニアを全方面で絶賛募集中です。 興味がある方は是非お声がけください!

speakerdeck.com

note.com

国内初?マルチリービングでランキングを勝手に自動改善!

f:id:rnitame:20200814180016p:plain

はじめに

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

さて本日は、「国内初?マルチリービングでランキングを勝手に自動改善!」ということで、マルチリービングという手法と、その手法を使ったランキングの自動最適化の方法についてご紹介したいと思います。なお、今回の取り組みは、筑波大学・図書館情報メディア系・准教授の加藤誠先生*1に大変ご助力賜りました。この場を借りてお礼申し上げます。

f:id:long10:20200629081915j:plain

目次

アローの不可能性定理

さて、マルチリービングのご紹介に入る前に、みなさんアローの不可能性定理*2なるものをご存知でしょうか?ちょっとWikipediaで調べてみたところによると、「投票者に3つ以上の独立した選択肢が存在する場合、如何なる選好投票制度であっても、個々人の選好順位を共同体全体の(完備かつ推移的な)順位に変換する際に、特定の評価基準(定義域の非限定性、非独裁性、パレート効率性、無関係な選択肢からの独立性)を同時に満たすことは出来ない。」とありました。

ちょっと何言ってるかわからないですね。

f:id:long10:20200625222933p:plain

要するに、この定理は次の5つの「公正さ」の基準を、常に同時に複数の人に対して満たすようなランキングなんて作れないよ、という主張です。

  • 人々の選好の順序は自由だ
  • 満場一致
  • 独裁者はいない
  • 他の選択肢から影響を受けない
  • 堂々巡りの矛盾にならない(a>b , b>cなら必ずa>c)

つまり、いつの世もみんなの心が一つなることは絶対にないというわけです、悲しいかな。

クラシルのランキングについて

前置きはこれくらいにして、クラシルには様々なランキング機能*3があります。

クラシルのランキング機能(ある日のランキング)

f:id:long10:20200625224611p:plain

ランキングの課題

クラシルのランキングは、現在利用していただいている多くのユーザさんの声によって作成されています。 しかしながら、先ほどのアローの定理が主張するように、ユーザさん全員に喜んでもらえるただ一つのランキングを作成することは無理なので、 この例の場合は、申し訳ないことにサバが嫌いな人にとって、今日のランキングは最低!ということになってしまうのです。(美味しいのにね。)

では、どうしたらいいか?

先ほどのアローの定理を再び思い出してください。ここで主張されているのは、列挙した5つの「公正さ」の基準を常に同時に複数人では満たせないということでした。

でも逆に言えば、

  1. 常に→適宜
  2. 同時→変化する
  3. 複数人→少人数

このような条件なら、完璧ではないにせよ、少しはユーザさんに喜んでもらえるランキングに近づけるんじゃないか? それが今回の取り組みのモチベーションになります。

マルチリービングって何なん?

マルチリービング(Multileaving)というのは、インターリービング(Interleaving)の複数版のことです。 それでは、インターリービングが何かというと、それはA/Bテストを手っ取り早く行う手法の一つです。

A/Bテストとはシステムの機能やデザインの良し悪しを判定するための取り組みのことで、 仮説検定やベイズ最適化を用いて評価するのが一般的です。(FirebaseのA/B Testing機能などを使うと便利ですね。)

f:id:long10:20200629082823p:plain

ただ、こういった方法だと、サンプル数を十分に確保できるまで評価できなかったり、評価方法が複雑になり良し悪しの判断が難しいという状況が起こったりします。 そうなると手っ取り早くAとBのどっちがいいか知りたいというニーズを満たすことができません。

そこで、このマルチリービング(インターリービングも同じ)という手法は、非常に高感度なランキング評価手法であって、 結果判定が可能になるまでの時間やサンプル数が比較的少なくても、AとBのどっちがいいか判断したいというニーズを満たすことができるという、 そんな非常に画期的な手法になります。*4 *5

さて、詳しく知りたい方は、脚注4の加藤先生のQiitaをご参考頂きたいと思うのですが、 この手法をめちゃくちゃ大雑把に説明すると、

  1. いくつか(たとえば4つ)の条件でランキングを作る
  2. いい感じにランキングをごちゃ混ぜにするアルゴリズムを使って、ごちゃ混ぜランキングを作る(初期は1/4ずつ均等に配分するなど)
  3. ユーザさんが、このごちゃ混ぜランキングをクリックした結果(暗黙フィードバック)を集計する
  4. 3で得られた結果から、最初に作った4つのランキングのうち一番強いランキングを評価して、その強いランキング(好まれた)の混ぜる割合をちょっと増やす
  5. 混ぜる割合を変えた状態で2に戻り、再びごちゃ混ぜランキングを作る

(以降、繰り返し)

このとき、このいい感じにごちゃ混ぜにするアルゴリズムの種類にはこのようなものがあります。

  • Balanced interleaving(均衡交互配置) (Joachims 2002a, Joachims 2003)
  • Team draft interleaving(チームドラフト交互配置) (Radlinski et al. 2008)
  • Probabilistic interleaving(確率的交互配置) (Hofmann et al. 2011)
  • Optimized interleaving(最適化交互配置) (Radlinski and Craswell 2013)

そして、今回使用したのは、これらをさらにいい感じにした版のPairwise Preference Multileaving(略してPPM)になります。*6

こちらは加藤先生ご自身がPythonモジュールとしてオープンソースとして公開されているのでご指導いただきつつ利用させて頂きました。*7

以下、加藤先生のモジュールを使用したPPMのデモ実装になります。 少しだけ詳しくみていきましょう。

まずは、ランキング対象となるダミーレシピデータとそのデータに合わせたスキーマクラスを用意します。 レシピ動画の素性を持つVideoクラス、ランキングを生成するRankerクラス、そしてユーザ情報を保持するUserクラスです。 Rankerクラスのrank関数では、与えられたランキングの条件ごとの割合とRankerの重みを線形結合しています。

class Video(object):
    def __init__(self, id, genre_ids, features):
        self.id = id
        self.genre_ids = genre_ids # レシピのジャンル
        self.features = features # 素性(ランキングの条件)

    def __hash__(self):
        return self.id


class Ranker(object):
    def __init__(self, keys, w):
        self.keys = keys # 素性(ランキングの条件)
        self.w = w # 重み

    def rank(self, documents):
        result = sorted(
            documents,
            key=lambda x: np.array([x.features[k] for k in self.keys]) @ self.w.T,
            reverse=True
        )
        return result


class User(object):
    def __init__(self, id, favorite_feature_id):
        self.favorite_feature_id = favorite_feature_id
        self.id = id

    def click(self, videos, click_cnt=3):
        # ユーザはfavorite_feature_idが高いものをクリックする
        return to_id(sorted(videos, key=lambda x: -x.features[self.favorite_feature_id])[:click_cnt])

videos = []

# ダミーレシピデータの作成
def create_videos():
    for i in range(10000):
        video = Video(
            id = i + 1,
            genre_ids = random.sample(genre_ids, 2),
            features = {fid: random.random()  for fid in feature_ids}
        )
        videos.append(video)


def gen_rand(size):
    m = np.random.uniform(low=-1.0, high=1.0, size=size)
    m /= (np.linalg.norm(m))
    return m

つぎに、generate_fluctuated_weight関数として、重みをちょっとだけ変更する(これを摂動と呼んでいます。)関数を定義します。 つまり、初期の重みは単位ベクトルに保存していますが、これを微変化させることで、混合するランキングの割合を調整しています。

# 閉区間[0,1]に収まるように制限する
def generate_fluctuated_weight(original_weight):

    unit_vec = gen_rand(len(original_weight))
    diff_vec = unit_vec * delta

    # 元ベクトルに加算、閉区間[0,1]からはみ出す場合は、はみ出る分を他に付け替え
    tmp = original_weight + diff_vec
    tmp2 = sorted([(i, _) for i, _ in enumerate(tmp)], key=lambda x: x[1])

    for i, v in tmp2:
        if v < 0.0:
            tmp[i] = 0.0
            tmp[-(i+1)] += v

    tmp /= np.linalg.norm(tmp) # 正規化
    diff_vec = tmp - original_weight
    unit_vec = diff_vec / delta

    return (original_weight + diff_vec, unit_vec)

get_superior_rankers関数は、ユーザさんのクリック状況から、一番強かったランキングを判定する関数です。

def get_superior_rankers(il_result):

    prefs = defaultdict(int)
    for res in il_result:
        for r in res:
            prefs[r] += 1

    winner_indexes = []

    for i in range(1, ranker_size):
        wins = prefs[(i, 0)] - prefs[(0, i)]
        if wins > 0:
            winner_indexes.append(i)

    return winner_indexes

そして、このupdate_ranking関数が実際にランキングを生成する関数になります。 現在の重みを摂動させて混合ランキングを作成します。 作成後に、摂動した結果の重みも更新します。

def update_ranking(user, genre_id):

    # メイン重みを取得
    res = get_parameter()
    w = np.array([_[1] for _ in res])

    target_keys_count = len(feature_ids)
    # unit_vecs = np.array([np.zeros(target_keys_count)] + [generate_fluctuated_weight(w) for i in range(ranker_size - 1)])

    weight_vecs = [w] + [generate_fluctuated_weight(w)[0] for i in range(ranker_size - 1)]
    rankers = [Ranker(feature_ids, _w) for _w in weight_vecs]
    top_videos = get_top_videos_per_genre(genre_id)
    rankings = [to_id(ranker.rank(top_videos)[:ranking_count_per_genre]) for ranker in  rankers]

    ppm = PairwisePreference(rankings)
    mixed_ranking = ppm.interleave()

    #  ランキング保存
    # 摂動後の重み更新

こちらのevaluate関数がランキング結果を評価する関数になります。 ユーザさんのクリックした結果を受けてランキング優劣を評価し、重みを更新します。

def evaluate(user, genre_id, click_video_ids):

    # 保存済み摂動重み群をもとに、競わせたランキングを復元
    res = get_ranking()

    weight_vecs = []
    for i in range(ranker_size):
        ranker_id = i + 1
        w = np.array([_[-1] for _ in res if _[0] == ranker_id])
        weight_vecs.append(w)

    main_w = weight_vecs[0]
    # print("main weight: %s" % str(main_w))

    rankers = [Ranker(feature_ids, _w) for _w in weight_vecs]
    top_videos = get_top_videos_per_genre(conn, genre_id)
    rankings = [to_id(ranker.rank(top_videos)[:ranking_count_per_genre]) for ranker in  rankers]

    origin_rankings = get_origin_ranking()
    mixed_ranking = [_[1] for _ in origin_rankings]

    # 復元ランキング達とクリック結果からマルチリービングで優劣判断
    # rankingオブジェクトも復旧する必要がある
    ranking = PairwisePreferenceRanking(rankings, contents=mixed_ranking)

    # クリック対象IDを混合ランキングのインデックスに変換
    click_indexes = [mixed_ranking.index(vid) for vid in click_video_ids]

    result = PairwisePreference.evaluate(ranking, click_indexes)

    # 勝者ランカーを決定
    winner_indexes = get_superior_rankers([result])
    main_w = weight_vecs[0]

    # 重み更新(勝者ランキングに用いた単位ベクトルの平均値を加算)
    if winner_indexes:
        diff_vecs = np.array([w - main_w for w in weight_vecs])
        unit_vecs = diff_vecs / delta
        main_w += alpha * np.average(unit_vecs[winner_indexes], axis=0)

        # メイン重み更新

    return main_w

ここまで定義した関数を、後は順番に呼び出すだけという形になります。

if __name__ == "__main__":
    update_ranking(user, genre_id)
    clicked_video_ids = get_click_data(user, genre_id) #クリック結果を取得
    evaluate(user, genre_id, clicked_video_ids)

こちらのデモ実装におけるシミュレーションでは、

  • 素性数(ランキングの条件)は4つ
  • 生成するランキングのダミーアイテム数は50に固定
  • ダミーアイテムは、4つの素性値をランダム生成したものを事前に大量生成

という条件に基づいてランカー数をnに決定しています。

また、1回のイテレーションで、ユーザさんは提示されたランキング(50アイテム)の中から、 1つめの条件が大きいものを順に3つクリックしたと仮定しました。 それを50イテレーション(=1セット)繰り返した後、第一素性の重みを記録したうえで、さらに20セット繰り返して、最終的に1個目の素性重みの平均を取得し、 それをランカー数:nの場合の「最終第一条件の重み」として評価しました。

その結果、このシミュレーションでは、このようにランカー数:2~20に変化させて行ったところ、次のような結果が得られ、 十分な収束を確認することができました。

f:id:long10:20200626001413p:plain

ランキング評価エコシステム

それでは、このマルチリービングをどのようにクラシルのランキングに反映したかについてご紹介したいと思います。 この仕組み全体をランキング評価エコシステムと呼ぶことにします。 導入したクラシルの機能は、特定の検索キーワードに紐づいて表示されるテーマ別ランキング*8という機能になります。

f:id:long10:20200626112356p:plain

マルチリービングを用いたランキングを自動改善する流れは、このような流れになります。

  1. こちらのテーマ(殿堂入り、時短、子供が喜ぶなど)一つ一つについて、複数のランキング生成条件(視聴数、新しい順など)に基づくランキングをいくつか生成します。
  2. ユーザさんを行動に基づいていくつかのグループ(クラスタ)に分類します。
  3. 各クラスタごとに1で生成したランキングからごちゃ混ぜランキングを生成します。
  4. クラスタごとにそれぞれごちゃ混ぜランキングを表示します。
  5. 1週間経って、ユーザさんがクリックした結果を集計します。
  6. 集計結果から、ユーザさんがこの1週間でもっとも好んだランキング生成条件を評価します。
  7. ユーザさんが好んだランキングを考慮して、ごちゃ混ぜにする割合をちょっと変化させます。
  8. 変化した割合が反映された状態で、再びごちゃ混ぜランキングを生成します。

つまり、ランキングを複数作ってABテストを行い、ABテストの結果を反映したランキングを毎週自動で作ることで、 ほんのちょっとずつランキングを最適化していこうという作戦になります。

なお、この機能実装については、こちらの先行研究を参考に行いました。*9

先行研究でのアルゴリズム

f:id:long10:20200626114737p:plain

こちらの研究では、オンラインでの評価が対象となっていますが、これをそのままクラシルに適応してしまうと処理コストが高すぎるし、 食コンテンツというのは、ニュースコンテンツなどとは異なり即時性をそこまで求められないことから、 今回は1週間に1回更新するバッチ処理として導入することにしました。 また、先行研究のアルゴリズムはTDM(Team draft multileaving)が採用されていますので、PPMを用いている部分も異なっていると言えます。

それでは、ランキング自動改善エコシステムをより具体的にご紹介したいと思います。

全体概要

f:id:long10:20200626102810p:plain

ランキング自動改善エコシステムは、以下の5つのSTEPで構成されています。

1. aggregate ステップ

Athenaを経由して以下のデータ収集・集計を行う ユーザさんの行動から1週間分(起動当日-7日)の特徴量を収集・集計する 1週間分のクリック数をユーザごとジャンルごとに集計し保存する

2. user clustering ステップ

ユーザさんの行動状況から、ユーザさんをk個のクラスタに分類します。 このクラスタごとにランカー(ランキング生成器)を生成していきます。

3. create ranking ステップ

aggregate ステップで収集したデータに基づいて、複数のランキングを生成します。 ランキングを生成したのち、現状のランカーパラメータの保存を行います。 ランカーパラメータの状況に基づき混合ランキングを生成します。

4. evaluate ステップ

ユーザクラスタごとジャンルごとにランカーの評価を行い更新します。 勝者ランカーのパラメータに対し割合を調整して保存します。

5. load ステップ

生成した混合ランキングをDynamoDBに格納します。

難しかった点、課題について

こちらの実装期間については、加藤先生のご指導とモジュールのおかげで、5日ほどでサービスリリースすることができました。 リリースしたばかりなので適合率の評価などは実際まだこれからですが、 導入までの難しかった点としては、マルチリービングがいかに少数のサンプル数で収束効果があるとはいえ、 今回非常にニッチな機能なので、摂動の影響は非常に小さく、それほど大きな効果を得ることはできなそうという点があります。 また、当初ユーザごとにパラメタを調整する設計だったのですが、やはり処理コストが肥大化してしまうので、 今回は、初回導入ということでユーザクラスタ*10 に対するランカーパラメータにすることで処理コストを抑えました。 今後の課題としては、このエコシステムを改善していくことで、ランキングに限らず他機能のA/Bテストにも活用できるようにしていきたいと思っています。 また、ランカー数を増やしたり、摂動するハイパーパラメタをチューニングすることによってさらに最適なランキングを生成し、 可能な限りアローの不可能性定理に立ち向かっていきたいと考えています。

まとめ

いかがでしたでしょうか?
今回は、Pairwise Preference Multileavingという手法を用いてランキングのA/Bテストを行い、その結果を反映してランキングを勝手にどんどん最適化させるという手法について ご紹介させて頂きました。 実際、ユーザさんに満足してもらえるような適合率にしていくためには、まだまだこれから改善が必要になると思っていますが、 アローの不可能性定理に人海戦術で対応していくのは、その名の通り不可能なので、 こういった技術や工夫を用いてクラシルを利用してくださるユーザさんにもっともっと満足してもらえる機能を提供・改善していきたいと考えています。

GASで作ったBotを負債化させないようにやってきた管理の仕方

f:id:rnitame:20200814180020p:plain

こんにちは。androidエンジニアと兼任でスクラムマスターをしているkenzoです。
スクラムマスターの業務において、メンバーや自分へのリマインド、バックログ整理の自動化、タスク状況の可視化などをGAS(Google Apps Script)を使って実施しています。
また、それ以外にもプロジェクトの進行管理やいろいろなことをGASでやってきました。(自分の健康管理とかも

GASを使うとちょっとしたスクリプトを書くだけで簡単にG Suiteのサービス(ドキュメント、スプレッドシート、カレンダー等)と連携することができます。
HTTP通信もできるので、APIを通じて他のサービスと連携させることも可能です。
そのため、うまく使えばかなりの業務を効率化させることができます。

簡単なことをやらせているうちは良いのですが、次第にいろんなことをさせたくなり、スクリプトは複雑化していきます。
永遠に自分だけで管理するのならそのままでも良いでしょう。
しかし、会社で業務としてやる以上は役割が変わったり退職したりと他の人がそのbotの面倒を見るようになることが考えられます。
その後任の方に辛い思いをさせないよう、きちんと整理しておく必要があります。

本記事では筆者が初めてGASを触ってから今まで作ってきたbotの管理方法の変遷について書いています。
コードの中身の話はありません。

GASのコードエディタ直書き + トリガーで定期実行

初めはスプレッドシートからGASのコードエディタを開き、そこにスプレッドシートの内容の操作やSlack等の外部サービスと連携するコードを直に書いていました。
そして、その処理を定期実行させるために作成した関数をトリガーに設定して運用していました。

f:id:kenzo_aiue:20200527181417p:plain

おそらく誰しも初めはここから始めたことと思います。

Chromeの拡張機能を使いGitHubでコードを管理

運用を続け、コードが増えてくるとGitHub等で管理したい気持ちが強くなってきます。
初めのうちはGASのコードエディタに書いたコードをローカルのファイルにコピペし、それをGitHubに上げていました。
なにか良い方法はないかと思い、見つけたのがChromeの拡張機能「Google Apps Script GitHub アシスタント」でした。

これを使うとコードエディタ上にGitHubでのコード管理に使うボタンが表示されるようになり、その場でpushやpullをすることができるようになります。

f:id:kenzo_aiue:20200618114812p:plain

当時はこれでずいぶん楽になったのを覚えています。

claspを使ったローカル開発

コード管理は楽になりましたが、当時は依然としてGASのコードエディタ上でコードを書いていました。
また、コードをコピペしてきてIDE上で変更してそれをまたコードエディタに戻したりというきつい運用をしていたこともありました。
その頃知ったのがGoogleが作ったCLIツール「clasp」でした。

このツールを使うとコードエディタのコードをコマンドでローカルに持ってきて開発することができます。
そして、ローカルで開発したコードは別のコマンドでコードエディタにアップロードして動かすことができます。(他にもできることはあります)
これを使うことで泥臭くやっていたローカル開発を手軽に行えるようになりました。

そして、いつからかTypeScriptに対応していたので、ローカルではTypeScriptで書けるようになりました。

トリガーをコードとスプレッドシートで管理

これまで多くのトリガーを作成して運用してきましたが、トリガーには若干扱いづらい部分もいくつかありました。

  • トリガーが作成したユーザーに紐付くため他のメンバーから見られない
  • 定期的に走るトリガーは○時〜○時の間としか指定できず、その間のいつ発火するかわからない
  • どのトリガーがいつ発火するのものなのか、コンソールで一つずつ開いてみなければわからない

GASにはトリガーを作る関数もあるのでそれを使うことにしました。
スプレッドシートにこのように実行したい関数と曜日、時間を指定します。

f:id:kenzo_aiue:20200618084818p:plain
(イメージです)

毎日0時を過ぎたくらいにこのスプレッドシートを元にトリガーを作成する処理を走らせることで、上記の問題も下記のように解決できました。
ちなみにこの処理のトリガーだけはコンソールで管理していました。*1

  • スプレッドシート(+コードも)の権限さえ付与すれば誰でもトリガーを管理できる
  • 指定した曜日、時間にトリガーを発火させられる
  • スプレッドシートを見ればどのトリガーいつ発火するのかひと目で分かる

Slackに送るメッセージをスプレッドシートで管理

リマインドすべきものが増えてくると、中には毎度同じメッセージを送っているものもあることに気が付きました。
そこで、上記のトリガーと同様にスプレッドシートで送りたいメッセージを送信タイミングを指定して管理することにしました。

f:id:kenzo_aiue:20200618111643p:plain (イメージです)

SlackのユーザーやグループのIDを用意しておけば、メンション付きのメッセージを送ることもできます。
こちらも毎朝早い時間にその日のメッセージを送るトリガーを作成して*2指定した時間に実行しています。

ちなみにこれはslackのremindでもそこそこ近いことができます。

スクリプト毎にclaspの設定を持たせて管理

スクラムに関するBotが多くの機能を持つようになってきて下記の理由のためにスクリプトエディタを分ける必要が出てきました。

  • 1日にトリガーの最大設定数20を超える処理を実行する場合がある
  • シートに設置したオブジェクトのクリックで処理の実行をさせるためそのシートのスクリプトエディタ上に処理を書く必要がある
  • それぞれが異なる役割を持っている(リマインド機能、タスクの可視化機能など、、)

スクリプトを分けてもスクラムに関するBotとして共通の処理もあるため、GitHub上では1つのリポジトリで扱い、ディレクトリ毎にclaspの設定ファイルを置いてアップロード先を分けて運用する形にしました。

各スクリプト共通で使う処理の括り出し

上記でスクリプト毎にアップロードする単位を分けましたが、Slackへの投稿をする機能のようにそれぞれのスクリプトで同じ機能を使う箇所があります。
分けた当初はその機能をファイルとして切り出してはいたものの、それを各ディレクトリにコピペして置いていました。
そのため、処理に変更を加えたければそれぞれのディレクトリ以下にあるファイル同じファイルに同様の変更を加える必要がある状態でした。

こちらへの対応としては、共通処理を書いたファイルをディレクトリに括り出し、アップロードの直前でそれらを移動して対象のファイルと一緒にするかたちをとりました。

|--common
|  |--slack.ts
|  |--github.ts
|--script1
|  |--.clasp.json
|  |--src
|  |  |--script1.ts
|--script2
|  |--.clasp.json
|  |--src
|  |  |--script2.ts
|--script1アップロード用(スクリプトで作成)
|  |--.clasp.json
|  |--common
|  |  |--slack.ts
|  |  |--github.ts
|  |--script1
|  |  |--src
|  |  |  |--script1.ts

package.jsonにファイル移動・アップロードをするスクリプトを作成し、それを使って運用しています。 (webpackでまとめてからアップロードすることもできそうでしたが、こちらの方法に比べて学習・作業コストが高そうだったため一旦見送っています。いずれ時間のあるときに試してみようと思います。)

まとめ

以上が今日に至るまでに実施してきたGASで作ったBotの管理の仕方でした。
どこまで管理してどのような運用をすべきかはBotを使う環境や扱う方の職種等によって様々かと思います。
こちらはあくまで一例ですが、Botの管理や運用の仕方で困っている方の一助になれば幸いです。


delyはエンジニア大募集中です。クラシルのエンジニアとざっくばらんに話をするお茶会・ランチ会なども実施していますので、興味のある方は是非ご応募ください!

*1:V8ランタイムで実行する際にバグっぽい挙動があったので注意してください(コンソールで作成したトリガーで実行する関数でさらにトリガーを作成するとそのトリガーが無効になる?ような挙動でした。あんまりやらないとは思いますが。)

*2:トリガー毎にメッセージを渡すことができなかったので、別シートに送信予定のメッセージを置いています。

クラシルAndroidプロジェクトの開発者体験の向上を頑張ってます!

f:id:rnitame:20200814180442p:plain

こんにちは、そしてはじめまして、今年2月にAndroidエンジニアとして入社したばかりのMeilCliです。先日プロジェクトマネージャーのtummyがユーザー視点でのクラシルAndroidアプリの改善を紹介しましたが、今回は自分が入社してから改善されてきた開発者視点でのクラシルAndroidアプリについてご紹介できればと思います

Detektの導入

クラシルAndroidアプリの開発にはKotlinコードを静的解析するツールとして今までktlintが使用されてきました。類似ツールとしてdetektというものがあり、このdetektにはktlintの解析ルールが収録されているため、解析ルールベースで考えると上位互換にあたります。そのため、ktlintからdetektへ移行することにしました

detektにはGradle Pluginが用意されているためAndroidプロジェクトへの導入はスムーズにできますが、クラシルAndroidのプロジェクトは現在マルチモジュールプロジェクトへの移行をしている最中なのでモジュールが複数存在しています。ktlintを運用しているときはマルチモジュールに対応した設定をしていなかったため、アプリケーションモジュールとなるappモジュールのみにktlintを実行している形になっていたのでdetektへの移行と同時にマルチモジュールに対応した設定を行うことにしました

各モジュールへの設定をいちいち書くのは面倒であり管理コストが増えてしまうので、今回はbuildSrcにGradle Pluginを作成し、各モジュールのbuild.gradleで作成したGradle Pluginを適用するだけでよい形にしました

まずプロジェクトのルート直下にbuildSrc/build.gradle.ktsを作成し、buildSrcのための設定を記載します

plugins {
    `kotlin-dsl`
}

repositories {
    jcenter()
    google()
    maven("https://plugins.gradle.org/m2/")
}

dependencies {
    implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0")
}

そうしたらbuildSrcがモジュールのように識別されるのでDetekt用のGradle Pluginのソースコードを追加していきます

buildSrc/src/main/kotlin/com/kurashiru/gradle/plugins/DetektConfigPlugin.kt:

package com.kurashiru.gradle.plugins

import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import java.io.File

class DetektConfigPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        project.extensions.findByType(DetektExtension::class.java)?.apply {
            toolVersion = "1.6.0"
            buildUponDefaultConfig = true
            parallel = true
            config = project.files("${project.rootProject.rootDir}/detekt.yml")
            reports.apply {
                xml.enabled = true
                val name = project.projectDir.toRelativeString(project.rootProject.rootDir).replace("/", "_")
                xml.destination = File("${project.rootProject.rootDir}/reports/detekt/${name}.xml")
            }
        }
        project.dependencies {
            add("detektPlugins", "io.gitlab.arturbosch.detekt:detekt-formatting:1.6.0")
        }
    }
}

buildSrc/src/main/resources/META-INF/gradle-plugins/DetektConfigPlugin.properties:

implementation-class=com.kurashiru.gradle.plugins.DetektConfigPlugin

このGradle Pluginではルート直下のdetekt.ymlをconfigとして指定し、CI用にルート直下のreports/detektフォルダーにレポートを書き出すようにしています

あとは通常と同じようにdetektのGradle Pluginの依存関係をルート直下のbuild.gradleに書いて、detektを適用するモジュールでは

apply plugin: 'io.gitlab.arturbosch.detekt'
apply plugin: 'DetektConfigPlugin'

この2行を追加すれば良いという形になりました

detektはホームページでたくさんのRuleSetsが紹介されるほど細かいところまで静的解析し、問題を検知・報告してくれるツールです。便利な反面、RuleSetsの設定をチューニングしなければ不要な警告を出してしまいます。そのため今後はクラシルAndroidアプリ開発に適したRuleSetsのチューニングをしていこうと考えています、あと最新版のdetektにバージョンアップしないとですね笑

GitHub Actionsの導入

クラシルAndroidアプリの開発では以前からAmazonのCodeBuildを使用してきました。CodeBuildではPullRequestでのビルドチェックやストアリリース時のリリースビルドを行っていましたが、それ以外でもCIで自動化してみたいよねということで、OSSでは無料でありプライベートリポジトリーでも無料枠の多いGitHub Actionsを部分的にですが導入していくことになりました

クラシルAndroidでは様々なGitHub Actionsのワークフローを運用していますが、そのうちの何点かご紹介します

PullRequestのタイトルチェック

クラシルAndroidでは社内的なリリースノート作成のためにPullRequestのタイトルに対して一定の規則を設けています。具体的には[improvement] タイトルのようにタグをprefixとして付けるだけですが、開発者がこの規則通りにPullRequestを作成できているかを人の手でチェックするのは大変なため、GitHub Actionsで自動化しました

name: プルリクのタイトルチェック

on:
  pull_request:
    types: [opened, edited, reopened]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: octokit/request-action@v2.x
        # 条件式は長いので省略しています
        if: startsWith(github.event.pull_request.title, '[improvement]') == false
        with:
          route: POST /repos/:repository/pulls/:pull_number/reviews
          repository: ${{ github.repository }}
          pull_number: ${{ github.event.pull_request.number }}
          event: "COMMENT"
          body: "PRのタイトルは`[タグ] .+`という形式にしてください"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

このワークフローではoctokit/request-actionを使用しています。これを使うことによってワークフローのステップにてGitHubのREST APIを楽に呼び出すことができるので、「こういう場合にこういうコメントを付ける」といったワークフローを作るときに便利です

PullRequestのマイルストーンチェック

タイトルと同じように、マイルストーンも社内的なリリースノート作成のために設定をルール化しています

name: プルリクのマイルストーンチェック

on:
  issues:
    types: [demilestoned]
  pull_request:
    types: [opened]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: octokit/request-action@v2.x
        name: Check PR Demilestoned
        if: github.event_name == 'issues' && github.event.action == 'demilestoned' && github.event.issue.pull_request != null
        with:
          route: POST /repos/:repository/pulls/:pull_number/reviews
          repository: ${{ github.repository }}
          pull_number: ${{ github.event.issue.number }}
          event: "COMMENT"
          body: "PRには必ずマイルストーンを付けてください"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: octokit/request-action@v2.x
        name: Check PR Opened
        if: github.event_name == 'pull_request' && github.event.action == 'opened' && github.event.pull_request.milestone == null
        with:
          route: POST /repos/:repository/pulls/:pull_number/reviews
          repository: ${{ github.repository }}
          pull_number: ${{ github.event.pull_request.number }}
          event: "COMMENT"
          body: "PRには必ずマイルストーンを付けてください"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

先にワークフローファイルをお見せするとこのような形で運用しています。octokit/request-actionを使ってコメントで通知するのはタイトルチェックと同じですね

さて、このワークフローではトリガーとなるイベントにissuesdemilestonedを使用しています。デフォルトブランチに直Pushする開発体系ならばこういったワークフローの作成を難なくできるかと思いますが、クラシルAndroidプロジェクトではPullRequestを介してデフォルトブランチにpushする形式を取っています
このワークフローを開発していたときのGitHub Actionsの挙動では、PullRequestで変更されたワークフローファイルはPullRequest上のCheckではpull_requestのイベントしかトリガーを引いてくれないという仕様になっている様子でした。そのため最初このワークフローを作成するときにissuesdemilestonedに対する動作を確認できなくてつまづきました。「マイルストーンが外された」や「マイルストーンが付いていない状態」をトリガーにするワークフローを作成したいときは是非参考にしていただければと思います

PullRequestに限らずIssueについても動作するサンプルをMeilCli/actionsに置いていますのでそちらもよければどうぞ

ちなみにですが、GitHub上でマイルストーンを貼り替える作業をするとdemilestonedのイベントが発火するらしく、このワークフローではそのままコメントを付けてしまうのでdelayをかけてAPIから情報を取ってくるなどの改善点があります。そのうち直そうと考えていますm(__)m

Detekt

さて、最初に紹介したDetektですが、PullRequestで自動でチェックしてくれなければ意味がありません。そこでGitHub ActionsでDetektを実行し、Dangerを使ってDetektの結果をコメントで通知するようにしました

クラシルAndroidではCodeBuildのほうでもDangerを運用しているためすでにGemfileDangerfileが存在していました。GitHub Actionsではそれらのファイルと共用するのは避けたかったため、.github配下にそれらのファイルを配置することにしました。Gemの解決にBundlerを使用している都合上、そのような配置をすると依存関係を解決できなくなるという状態に陥ったため、過去に自分が勉強がてらに作成したMeilCli/danger-actionGemfileの位置にとらわれないように改修して使用しています

.github/Gemfile:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem 'danger'
gem 'danger-checkstyle_format', '~> 0.0.1'

.github/Dangerfile:

github.dismiss_out_of_range_messages

checkstyle_format.base_path = Dir.pwd
report_files = Dir.glob("reports/detekt/**")
for report_file in report_files do
    checkstyle_format.report report_file
end

.github/workflows/ci.yml:

name: CI

on: [pull_request]

jobs:
  detekt:
    runs-on: ubuntu-latest
    env:
      GITHUB_USER: "github-bot"
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review'
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.6'
      - uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - uses: actions/cache@v1
        name: Cache Gradle
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}-${{ hashFiles('buildSrc/src/**') }}
          restore-keys: |
            ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}-
            ${{ runner.os }}-gradle-
      - uses: actions/cache@v1
        name: Cache Bundler
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('.github/Gemfile') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      - name: Grant permission
        run: chmod +x gradlew
      - name: Run Detekt
        run: ./gradlew detekt
      - uses: MeilCli/danger-action@v4
        name: Run Danger
        with:
          plugins_file: '.github/Gemfile'
          install_path: 'vendor/bundle'
          danger_file: '.github/Dangerfile'
          danger_id: 'danger-detekt'
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

さて、GitHub ActionsやDangerを運用されている方々にとってはあまり目新しいワークフローではないかもしれませんが、Detektを導入するにいたって、開発者がPullRequestに変更を加えてから静的解析のコードレビューがされるまでの時間を減らす点でこだわることにしました

具体的にはactions/cacheを使って依存関係やタスクの実行結果などをキャッシュすることによって、キャッシュが存在する場合にキャッシュを利用することで早く動作させるようにしました

f:id:meilcli:20200617170748p:plain
GitHub ActionsのCIワークフローの実行一覧
結果としては遅ければ5分程度、早ければ2分程度、平均3~4分で実行が完了するようになったためキャッシュを利用してワークフローを高速化する試みは成功だったかなと思います
(参考数値としてktlintの頃はいろいろな処理を待機していたために約8分ほどかかっていました)

今後について

開発者体験という視点で言えばクラシルAndroidプロジェクトはたくさんの課題があります。その中で直近でやろうと考えてるものをご紹介し、これを次回予告にします

  • 長年放置し続けてきたlintの対応、不要になったリソースの削除
  • 使用しているOSSなどのライセンス表記のための集計ツールをMeilCli/Librarianに置き換える
    • これによってライブラリーやライセンス通知の集計精度が向上する見込みです
  • ライブラリーアップデートの自動検出・通知
  • モジュールのbuild.gradleの共通な宣言部分をbuildSrcへまとめる

note.com

最後にdelyではエンジニアを大募集しています。ランチ会などを定期的に開催してるので、中の人と話してみたい!という風に興味がある方は是非お声がけください

クラシル Android アプリの改善をがんばっています!

こんにちは、クラシル Android のプロダクトマネージャーをしている tummy です。dely では今年 4 月から本格的にチームが立ち上がり、Android アプリの改善に取り組み始めました。スタートから 2 ヶ月弱経ち、徐々にアプリ内も変化してきたのでスクショを交えつつ取り組みについてご紹介できればと思います。

まずはデザインのお話から。

検索バーが変わりました

f:id:rnitame:20200611210917p:plain

ホーム上部にある検索バーですが、パッと見ここから検索できるように見えないのではないかという仮説のもと、人気キーワードを定期的に変える形で表示してみたり、検索ボタンを明示的に表示するようにするなど様々な UI を試しました。結果、検索バーを固定したパターンでわずかなタップ率の向上が見られたため現状はこのパターンになっています。あまり数字は変わりませんでしたが、こういった地道な改善も行っていっています。

お気に入り検索結果画面からフォルダ追加できるようになりました

f:id:rnitame:20200611210437p:plain

今までお気に入りの検索結果画面からフォルダに追加することはできませんでした。しかし、ユーザーさんからのご要望が多かったため機能を実装することにしました。少しずつですが使われている数が増えているので嬉しいです。ぜひご活用ください! 改善ブログに使い方も載っているので良ければこちらも御覧ください 💁‍♀️

note.com

レシピ詳細の材料部分にアイコンがつきました

f:id:rnitame:20200611211314p:plain

以前からここはタップできる領域で、材料名で検索できるようになっていて使ってくれているユーザーさんも多いことがわかりました。そこで、上2つの材料名に虫眼鏡のアイコンをつけることで「ここはタップできて、タップすると検索に飛べる」という認知を持ってもらえるのではないかと考えました。結果、認知を持ってもらえたのか、以前より材料検索を使う方が増えてくるようになりました。

通知の設定が変わりました

f:id:rnitame:20200611212953p:plain f:id:rnitame:20200611213037p:plain

今までは通知をオフにするとすべての通知が届かないようになってしまっていました。 しかし最近のアップデートで、以下の項目ごとに通知のオン・オフを設定できるようになりました。

  • お得な情報(キャンペーンなど)
  • チラシ
  • 質問への返信
  • 公式アカウントからのメッセージ
  • おすすめレシピ
  • その他(以上の項目に属さないものすべて)

そのため、チラシの通知はいらないけどおすすめレシピの通知はほしい!といった場合にも対応できるようになります。


内容変わって、技術的なお話もさせてください。

minSdkVersion をアップデートしました

minSdkVersion を 16 から 19 に引き上げました。クラシルでは一度もあげたことがなかったので、ついに!といった感じです。今回はあまりコードを削除できた部分はありませんでしたが、今後徐々にあげていって古い OS 向けに書いているコードも消せるといいなぁと思います。

f:id:rnitame:20200609135208p:plain
minSdkVersion を上げるリリースをした際のリリースノート

改善がんばってます!

分析したデータ、ユーザーインタビューやアンケートの結果など、たくさんの情報を活用して改善を進めています。(インタビューやアンケートにご協力いただいたユーザーの皆さまありがとうございます!)

今後も検索機能を中心に改善を進めていくほか、プッシュ通知の改善にも着手していく予定です。また内容についてはブログ等でお知らせできればと思います。

最後に、dely ではエンジニアを全方面で絶賛募集中です。 興味がある方は是非お声がけください!

speakerdeck.com

note.com

CSエンジニアになって3ヶ月経ちました

f:id:sakura818uuu:20200402165707p:plain

はじめまして。開発部の sakura818uuu です。 CS(カスタマーサクセス)チームのサポートエンジニアを始めて3ヶ月が 経過しました。

3ヶ月が経ったので振り返ってみようと思います。

主な業務内容

主な業務内容はCSチームの技術的サポートや施策の計画や管理をしています。
また、CS業務のドキュメントの拡充や整理を行いました。

やってること(一部)
・お問合せの技術的サポート
・CSに関する施策の計画・実行管理
・CS業務の言語化

お問合せの技術的サポートはブログも書かせていただきました。 tech.dely.jp

求められるスキル

3ヶ月働いてみて実際に感じた求められスキルを3つ記載します。
データ分析(SQL) 、プロダクトの仕様把握 、情報共有です。

1.データ分析(SQL)

データ分析は言わずもがな必要になります。
SQLはCSエンジニアになる前もデータサイエンスチームに所属していたため書いていました。

ただ、データサイエンスチームでは「全体の何%が」と森を見ることが多かったのですが、CSは逆で個別のデータログなど木を見ることが多いです。
どちらも経験したからこそデータの仕様についての理解が一段と深まったように思います。

2.プロダクトの仕様把握

ユーザーさんから多種多様なお問合せをいただくため、プロダクトの仕様把握は最重要です。
運がいいことに私にはかなり合っている仕事でした。なぜなら入社してから2年間テスター(のようなこと)をしていたから仕様把握が既に細かなところまで出来ていたからです。

新機能やバージョンアップに伴う仕様把握も重要です。
この3ヶ月はリリースのサイクルが非常に速かったため、開発状況から常に目を離さないようにしていました。
新機能でわからないところや不明点があると時折エンジニアに確認したりして仕様認識の齟齬を起こさないよう気をつけました。

3.情報共有

情報の共有はどんな仕事でも必ず必要ですが、特にCSではこまめな情報共有が必要となります。

・主にユーザーさんから届いたバグや不具合の情報を開発者に伝える ・新機能や不具合修正などの開発状況をCSチームに伝える

この2つの情報共有は欠かせないです。

また、全体的なお問合せの傾向を把握できるよう
お問合せ内容をカテゴリ別に分類し月別にどんなお問合せが何件届いているのかを可視化するようにしました。

情報共有をまめに行うことのできる人がCSエンジニアには向いているんだな、と思いました。

他にもあったら良さそうなスキル

他にもあったら良さそうだと感じたものです。
今はテクニカルライティングを書籍やネットを通して学んでいます。

・テクニカルライティング
・原因を突き止めるために問題の切り分けを上手くできること
・少ない情報からあらゆることを想像できること

キャリアとして

3ヶ月経ってみてキャリアとして考え方が変わりました。
CSエンジニアの前は検索エンジニアをやっていて、「これからもずっと検索に携わっていきたいな。いくんだろうな」と思いきっていたからです。

CSエンジニアになってみて、CSエンジニアとして歩んでいくのもいいなと思いました。

さいごに

CSエンジニアになって3ヶ月経ち大分慣れてきました。
まだ土台が出来た状態にすぎないので、これからCSエンジニアとしてどんなことができるか模索していきたいと思います。


delyではエンジニアを絶賛募集中です。
興味があるかたは是非お声がけください!