AppBrew Tech Blog

AppBrewのエンジニアチームの日々です

【ユーザーファーストで】LIPS の検索システムを改善する【考える】

AppBrew でアルバイトをしている Pin(@spinute)です。先月は【無職が】AppBrew でアルバイトをしてみた【やってみた】という記事で私がこの会社で働いている経緯を説明しました。

今日はもう少しテックブログらしい内容で、LIPS の検索機能改善について紹介します。

小規模なチームでは、検索機能専任の人はおらず、開発者が片手間に検索機能のお世話をすることが多いと思います。 そうするとイマイチ勘所がわからず、どこに着目し・どう手を加えればいいのか、迷ってしまうこともあると思います。 そういう場面で改善の指針となる評価指標を、実例を混じえながら紹介していきます。

また、背景にある考え方は検索システムに限らないものなので、検索を担当するかどうかによらず開発者の方は参考にしていただければ幸いです!

LIPS の検索機能

検索システムは、ユーザの入力した文字列(クエリ)に基づき検索結果を返します。構造化データの入った RDB と、文書検索ができる Elasticsearch などの DB を組み合わせて実装することが多いと思います。LIPS もそうです。

LIPS の場合、検索対象は商品や投稿、ユーザやブランドなどです。単に商品名やブランド名で検索するだけなら、そのものを見つければ良いのですが、ユーザはフリーテキストで探しているものを指定します。その結果、構造的な検索だけでは検索結果を上手く見つけられず、またそもそも何が良い検索結果なのかが曖昧です。

例えば「日焼け止め」と入力されれば良さげな日焼け止め商品を見繕い、「ニキビ」と入力されればニキビ対策をしたいのだと見抜き、「みきぽん」と入力されれば「かわにしみき」さん*1を見つけられる必要があります。

f:id:spinute:20190527143814p:plain

LIPS は3月に300万ダウンロードを突破し、現在までに80万件超のクチコミが投稿されています。ユーザがコスメやそのクチコミを見つける体験を支えるためには、お利口な検索システムが必要です。

指標の設計に使えるログの例

データ駆動な開発プロセスに馴染みのない方もいるかもしれないので、ログの話もしておきます。

LIPS のログ収集基盤はスタートアップでも出来る分析基盤で社長が紹介してくれたものです。ユーザの行動ログを記録し、それを集計して指標を計算できます。

例えば以下のようなログがあります。

  • どのようなクエリで検索したか
  • どの検索結果を見て、どれを選択したか
  • 遷移先にどのくらいの時間滞在したか

以降では、このようなログを使って、検索機能の評価に使う指標を設計していきます。

主要な指標、副指標

検索システムの文献を読むと、その評価はしばしばクエリと結果の関連度(relevance)を中心に説明されます。*2 他にも、レイテンシやメモリ使用量、あるいはユーザビリティなどの、一般的な指標も使われます。

一方、仕事でアプリを作るときには、ユーザ数や収益などの経営レベルの指標のことも考えなければいけません。

そして、これらの指標の間にはしばしばトレードオフがあります。

仕事では主要な指標としては経営指標を置き、これを改善するための手段として他の指標を参照するのが良いと思います。

LIPS はユーザからお金を貰うアプリではないので、ユーザ数・滞在時間・RR などを主要な指標としています。*3

主要な指標はゴールを明確にします。一方、プロダクト寄りのより細かい指標があると、個々の施策を仮説検証しながらゴールに近づいていくために役立ちます。この指標を副指標と呼ぶことにします。*4

副指標だけを見ていてはいけない

副指標に関する注意点として、それだけを見ていてはいけません。具体的な施策のことを考えていると視野が狭くなりがちですが、ゴールは主要な指標を改善することであり、副指標はあくまでそのための参考になる値として利用すべきです。

例を挙げます。CTR(Click Through Rate のこと。インプレッション数あたりのタップ数)は汎用な評価指標です。以前は AppBrew でも、検索結果の CTR 改善を目指していました。しかし、この数ヶ月、検索改善施策を経て CTR は日に日に下がっていきました。

施策を打つたびに下がっていく切ない CTR の様子
施策を打つたびに下がっていく切ない CTR の様子

この原因は検索結果を出せていないクエリが元々多くあったことです。 そのようなケースで検索結果を出せるよう改善を進めた結果、インプレッションが増え、CTR が下がりました。 一方で、検索結果にたどり着ける人は増えたので、この施策は成功でした。

f:id:spinute:20190527145057p:plain:w300
検索結果にたどり着けた人の割合

評価指標を設計する

デカルトは「方法序説」にて「困難は分割せよ」と言っています。問題解決の理論であるアルゴリズム論でも、分割統治は主要な設計パラダイムのひとつです。ここからわかる、主要な指標からより扱い易い副指標を導く一般的なアプローチは分割することです。

例えば、アプリの収益モデルを KPI に分割したものである KPI ツリーは、この発想に基づくものです。

https://d1b8ifz9kd2pqh.cloudfront.net/2016/02/Retention-Tree-Full-1-979x1024.png
アプリの KPI ツリーの一例(https://growthhackjournal.com/monetize/kpi-tree-for-app より引用)

では LIPS 検索システムの評価指標はどのように分割できるのでしょうか?汎用的な着眼点をいくつか紹介します。

ファネルで分割する

検索ユーザの動線を時間軸に沿って分割したファネルをもとに、副指標を設計できます。

LIPS の場合、ユーザは以下の手順で検索機能を使います。

  1. 検索窓をタップする
  2. 検索クエリを打ち込む
  3. 検索結果リストを眺める
  4. 興味があるものをタップする
  5. 見つけた結果を眺める

f:id:spinute:20190527005900p:plain

ファネルでの分割は、ユーザインパクトの見積もりにも役立ちます。

  • 前半ほどユーザが多いため、前半の改善ほどより多くのユーザに影響を与える
  • 離脱率が高い箇所ほど、改善の余地が大きい

定義式で分割する

例えば以下のような定義式を使って、副指標を設計できます。

  • 検索滞在時間 = 検索試行回数 * 検索試行あたり平均滞在時間
  • 検索試行回数 = 検索利用者 * 平均検索利用回数

特徴で分割する

ある側面に着目しログを分類することで、副指標を設計できます。

  • 検索利用者 = 新規ユーザ + 既存ユーザ
  • 検索実行者 = 結果をタップした人 + 結果をタップしなかった人(検索離脱者)

このとき分割の仕方によって結果の見え方が変わることには注意が必要です。

  • 新規ユーザと既存ユーザは、利用開始日からの日数にしきい値を設けて区別します。新規ユーザが定着していく様子は連続的であり、しきい値は恣意的なものです
  • 検索実行者をタップの有無で2値に分けるのではなく、タップの回数によって分類することもできます

https://1.bp.blogspot.com/-vI7t2-ddeZM/Wat2bTFu4UI/AAAAAAABGXw/P5C6jJfReNYaTz9GxIEMRg62CWvXZ_nZQCLcBGAs/s400/egg_separator_egg.png
卵の黄身と白身を分割するエッグセパレーター(いらすとや より)

LIPS 検索システムの評価指標

最後に、LIPS 検索システムの評価指標を具体的に紹介します。

まず、検索機能のゴールは、ユーザが対象を見つけ、その内容に満足することです。つまり、検索機能のユーザ価値を、ファネルに沿った検索動線の達成度または達成割合で定量化できます。そこで「検索試行あたり結果ページ滞在時間」を検索機能のユーザ価値としました。

副指標としては、例えば以下のものを設定できます(太字は特に重要だと思うもの)。

  1. 検索窓をタップする
    • 検索機能の利用率
    • 検索機能の平均利用回数
  2. 検索クエリを打ち込む
    • 検索候補のタップ率
    • 検索候補により入力を省略できた平均文字数
  3. 検索結果リストを眺める
    • 1件以上の結果を表示できた割合
    • 1ビュー以上の結果を表示できた割合
  4. 興味があるものをタップする
    • 検索離脱率
    • 検索試行あたりタップ数
    • CTR
  5. 見つけた結果を見る

余談ですが、指標を計算する際に使うログはユーザの鏡です。ユーザファーストの実践を目指す身として、ログを丁寧に扱い、深く理解することは欠かせないことだと日々感じています。

おわりに

エモいことを書いたついでに、takram の Shenu: Hydrolemic System という作品を紹介します。

www.youtube.com

これは「荒廃した未来の世界における水筒」をテーマにした作品だそうです。

水質汚染等により供給可能な水が極端に限られた世界では、現状の延長上にある水筒を考案することが非現実的に思われました。そこで、このような差し迫った環境において、人間が一日に排泄、排出する水分を極限まで少なくできれば、そもそも人体が必要とする水分を少なくできるのではないか、という結論に達しました。これが最終的に、人工臓器を含む新しい一連のプロダクト群に結実したのです。水筒を作るのではなく、人間の体を水筒と捉え、生存に必要な人工臓器を提案しました。このように、課題自体を見直すことにより、水筒という課題に対する新しい解釈が生まれたのです。

https://ja.takram.com/projects/shenu-hydrolemic-system/ より

問題を解くことと同じくらい、あるいはそれ以上に、正しく問題を定義することは重要です。そしてそれは難しく、また知的に面白いことでもあります。AppBrew では、データを見ながら事業目線で開発に取り組める仲間を引き続き募集しています!

*1:有名 YouTuber。誕生日が僕と同じなことで有名(ではない)

*2:例えば、Information RetrievalIntroduction to Information Retrieval が二大有名教科書ですが、いずれも relevance を中心に説明されています。

*3:余談ですが、AppBrew では会社・チーム単位で主要な目標を明文化しています。特に Measure What Matters という書籍で一躍有名になった「目標と成果指標(OKR, Objectives and Key Results)」というフレームワークを使っています。このテーマについては、原著または Google の公開している資料が参考になるので、興味のある方は是非参照してみてください。

*4:副指標は一般的な用語ではなく、ここで説明のために僕が勝手に定義した単語です。

デザイナーが1人でABテストを回してWEBの直帰率を10%以上改善した話

こんにちは!去年の10月からAppBrewでUIUXデザイナーをしている 西山(@Fav_KudasaiTT) です。普段は事業の数値を改善させるため、LIPS WEBのデザイン改善したりコード書いたりディレクションしたりしています。

事業の数値を改善させると言っても、取り組むべき課題を見つけその課題に対して精度の高い仮説を立てて検証を回すというフローがありますよね。

今回は検証を回すという部分に絞り、LIPS WEBでABテストを回して直帰率を中心にユーザー行動を大幅に改善した話をさせていただきます。

デザイナー1人でABテストできるツール

デザイナーがABテストをしようとすると、次の課題にぶつかるかなと思います。

  • ABテスト基盤を作成する必要があり、結果の統計処理にも手間がかかる
  • 最速で回したいがdeployに時間がかかる

そこで、弊社では Google Optimize というABテストツールを使っています。

Google Optimizeには以下の特徴・メリットがあります。

  • 導入が楽
  • ツール上でHTMLやCSS、Scriptを編集できる
  • ターゲットやトラフィック割り当てを細かく指定できる
  • 評価指標を設定すると有意差の検定をしてくれる

f:id:sglv:20190516131825p:plain

このようにGoogle Optimizeは、細かいターゲット設定とビジュアライズが可能な上に、テスト基盤を作らなくて済むし面倒な統計処理をやってくれる素晴らしいツールなのです。

運用例

この節では、Google Optimizeを使ったABテストの手順をより具体的に説明します。

  1. 検証する仮説を決める
  2. パターンとターゲットを設定する
  3. 評価指標を設定する
  4. リリースして結果を見る
  5. 結果が良かったものを反映する
  6. 1~5を繰り返す

今回は、クチコミ詳細ページの改善事例を使って説明します。 弊社はコスメのクチコミアプリを作っており、WEBでもクチコミを表示する画面のUXは非常に重要です。

1. 検証する仮説を決める

まずは、検証する仮説を決めます。このとき、定性的な仮説を定量に落とし込んだ改善数値も予想します。

# 対象ページ
- クチコミ詳細ページ

# 仮説
- 関連クチコミへの距離を短くしてクチコミ一覧を見せることにより、直帰率が下がるのではないか
(クチコミが検索流入ユーザーのニーズに合わない可能性があるため)


# 改善数値予想
- 3~5%

f:id:sglv:20190515234428p:plain

f:id:sglv:20190516131847p:plain
ユーザー行動の変化予想

2. パターンとターゲットを設定する

次にパターンとターゲットを設定します。

# パターン
①本文のクチコミ画像を小さくする
②クチコミ本文のプレビューを短くする

パターンは2個から5個を目処に作っていきます。今回の例ではパターンごとにOptimize上のエディタでCSSを追記します。 ターゲット設定は、URLとデバイスを指定しています。

f:id:sglv:20190515234520p:plain

パターン①の「本文のクチコミ画像を小さくする」では、横に並んだ画像のサイズを調整しています。CSSを数行書いて終わりです。

パターン②の「クチコミ本文のプレビューを短くする」では、本文プレビューの高さを調整しています。こちらもパターン①と同じでCSSを数行書くだけで済みます。

3. 評価指標を設定する

評価指標を設定するにはGoogle AnalyticsをOptimizeに紐づける必要があります。 評価指標は、メイン指標・サブ指標を3つまで設定できます。

今回はメイン指標として「直帰率」を設定しました。 そして、直帰率が下がったとしても、セッション時間やページビュー数が下がっては良くないので、サブ指標として「セッション時間」「ページビュー数」を設定しています。

4. リリースして結果を見る

トラフィック割り当てを設定し、リリースします。検証を早く終えたい場合は、割り当てるユーザーを多くします。

以下の画像はGoogle Optimizeのレポートの一部で、ページあたりセッション数の変化を表しています(値は実際のものではなくダミーです)。 このように、Google Optimizeは自動で統計処理をし、変化に有意差があるかどうかを知らせてくれます。

f:id:sglv:20190515234542p:plain

弊社では、Google Optimizeでの検証をするだけでなく、Google Analyticsでランディングページを絞っての検証もしています。

このケースでは、設定した指標のうち直帰率の有意差をOptimizeで確認するには時間がかかりそうだったので、Google Analyticsでランディングページを絞り、最適なパターンを検証しました。

5. 結果が良かったものを反映する

有意に改善成果が出た変更について、開発環境でコードを書き、PRを送ってエンジニアにレビューしてもらいマージします。 OptimizeでCSSコードを書いている場合と同じように書けばいいので、あまり時間はかかりません。 変更を反映した後はGoogle Analyticsで指標を再度確認します。

6. 1~5を繰り返す

あとは、今までの流れをページやコンテンツごとに繰り返します。 大きな仮説を作り、それをブレイクダウンして適切な仮説粒度にしながら検証することで、迅速に改善サイクルを回せます。

まとめ

Google Optimizeを使ってABテストをすることで仮説検証サイクルが早くなり、結果的にLIPS WEB全体の直帰率をこの3ヶ月で10%以上改善できました。

デザイナー1人で仮説から検証まで行えることで、コミュニケーションコストも小さくできました。 ABテストして結果が良かったものだけをコードに反映するため、無駄な開発工数を削減でき、エンジニアはより高度な課題に集中できるようになりました。

このように私達は常日頃から仮説検証のプロセス改善を意識し、最小・最速で検証できるよう心がけています。 これからもユーザーにとっていいサービスを提供できるよう精進していきます!

We Are Hiring

AppBrewでは定量・定性に真摯に向き合ってプロダクトを開発したいメンバーを募集しています!ご興味がある方は転職ドラフトやWantedlyよりぜひご応募ください!

また、広告クリエイティブを仮説検証したいデザイナーインターンも募集しております!作ったCRに対して定量・定性的なフィードバックができる体制が整っていておすすめです!

【無職が】AppBrew でアルバイトをしてみた【やってみた】

AppBrew でアルバイトをしている @spinute と申します。

突然ですが、先日リリースされてエンジニア界隈で話題になっていた LAPRAS のサービス、みなさん試してみましたか?

f:id:spinute:20190418181343p:plain
私のページ。ビジネス力・発信力が足らないらしい。仰るとおりです。

先日そんな LAPRAS でインターンをしている方のブログを目にしたのですが、LAPRAS の組織文化やミッションステートメントを上手に伝えられていてとても素敵だと思いました。

そこで、今日は AppBrew のそれを伝えるべく、仕事や意思決定のすすめ方、組織文化が作られている様子などを私の目線から紹介してみます!

AppBrew にたどり着くまで

まず、私は現在フリーターです。どういうことかというと、2月に前職を退職し、次の就職先を探しながら AppBrew でアルバイトをしています。

転職することは半年以上前に決めていたので、仕事を辞める前に次の仕事を探してもよかったのですが、腰を据えて転職活動をしたく、無職も体験してみたかったので、仕事を辞めてから求職活動をすることにしました。

スタートアップ企業はその定義からして革新的で多様性が大きく、これは裏返すとアタリハズレが激しいということでもあります。 今回の転職ではスタートアップ企業に行くことも考えていますが、ほんの数回の採用面接や説明会で、果たしてそこで働く自分を正しくイメージできるのでしょうか?*1

「思ってた仕事と違う...」「人間関係が辛い...」「本当はあそこの部署に行きたかったのに...」といった就職ガチャ問題の対策としては、就職する前に働いてみるのが有効だと考えています。*2

そのため、私はとりあえず仕事をやめて、アルバイトとして雇ってくれるスタートアップ企業を探すところから転職活動をスタートしました。*3

退職ベストプラクティスや転職活動体験記は、この転職活動が一段落した頃にまた自分のブログに書こうと思っています!

AppBrew で私がやっていること

そうして AppBrew でアルバイトをすることになり、私は LIPS の検索機能改善に取り組んでいます。 LIPS の累計ダウンロード数は先月 300 万を超え、着々と増え続ける商品やクチコミを見つけるため、検索・レコメンド・ランキングなどの重要性が高まっています。

具体的な検索改善施策や検索アーキテクチャは、ボリュームも大きくなりそうなので、成果が溜まった頃にまた別の記事として紹介するつもりです。

そのためここでは、参考にしている資料をいくつか紹介するにとどめておきます。

仕事の方法論

AppBrew における開発のすすめ方は、先日の記事でも紹介されていますが、一言でいうとちゃんとデータを見て意思決定をしています。

AppBrew のエンジニアは以下のサイクルで仕事を進めます。

  1. 問題を分析して仮説を立てる
  2. 改善案を出し、そのインパクトを見積もり、重要なものを実装する
  3. 効果を検証し、報告・共有する

データに基づいて開発を進める利点は、各々が独立して、客観的な意思決定を精度高く行えることです。

エンジニアリングチームをスケールさせるには、作業の依存や同期を小さくしなければなりません。*4 AppBrew では週次ミーティングでの報告以外に同期的な仕事はなく、その結果として働く時間・場所についての制約がありません。 自走力がありセルフマネジメントできる人を揃えることで、このようなチームビルディングに成功しているように感じました。*5

AppBrew に入ったあとの印象

AppBrew の組織文化は社内で継続的・意識的に育てられ続けているものです。 社内の Slack や Qiita にある記録は、(アルバイトの私であっても)基本的にはすべて見ることができます。*6

採用の際にもカルチャーマッチを重視していて、カルチャーには一家言ある人々がカルチャーにカルチャーを重ねた最強のカルチャーが実践されている様子を日々目の当たりにしています。

社長の意思決定速度がヤバかった事例

ここでひとつ、社長の意思決定速度がヤバかった話を紹介します。*7

先日社長と 1 on 1 をしたときに「Indeed の新卒給スゴいですよねー」という話をしました。

すると、2時間後にはエンジニア全員の給与が1割増えていました。なにを言っているのかよくわからないと思いますが、2時間後にはエンジニア全員の給与が1割増えていました

AppBrew では全社員の給料を公開しています。また、定期的に360度評価を実施し、業績やパフォーマンスに応じて給料を調整しています。最近ソフトウェアエンジニアの給与相場が上がっているのに対して、社内での給与調整が追いついていないのではないか、という意見が前回の評価の際に多く出ていたそうです。AppBrew が求める人材のレベルやスキルセットを考えると、競争力を高めるためには給与を上げるべきであり、また、大きな価値を生み出せるプロダクトを作る覚悟を社員に持たせる意図もあって、給与アップに踏み切ったそうです。

1 on 1 をきっかけに、これらの議論をまとめ、財務責任者やエンジニアチームに相談し、全体に共有し、fix するまでが2時間で実際に起きたことです。ちなみに、今回の調整分は賞与として支給される*8そうで、スピード感があるだけでなく細かい配慮も抜かりないです。

意思決定に承認がいらない

そもそも社長は意思決定が早い*9人なのですが、AppBrew という組織そのものが、意思決定を迅速に行えるよう設計されています。

性善説に基づいて全員に大きな裁量が与えられており、社内の情報をとにかくオープンにすることで、スピード感を持ちながらも精度の高い意思決定を各々が行えるようになっています。*10

一般的な会社だと、意思決定は「偉い人」がするものだと思います。 しかし AppBrew では承認プロセスを極力排して、各々が意思決定をしています。 意思決定に必要な情報を公開*11し、社員が互いを信頼し、さらに行動指針として承認なしでの意思決定を推奨することで、このようなドラスティックな組織文化を実現できているのだと思います。

例えば「これ買っていいですか?」->「そのくらい相談しないで買ってください」というやり取りを何度か Slack 上で実際に見かけています。

成果で評価して、やり方を管理しない

社内にはいろいろな人がいますが、(成果を出せる範囲で)働く場所や時間は各々に委ねられています。これもまた、社員が互いを信頼し、管理コストをかけないことで、チームとしてのパフォーマンスを高める AppBrew の素敵な組織文化だと思っています。

例えば、カフェで仕事をしている人もいれば、講義・研究室とオフィスを行ったり来たりしている人*12、夕方ごろ出社してくる人、土曜日に「今日は仕事しに来たんじゃねえんだよなー」と言いながら現れて Amazon から届いた東方を開封し、プレイして帰っていく人などがいます。

ここで紹介した会社の文化や行動指針はほんの一部で、実はまだまだ色々あるのですが、近い内に社長が発信していく気配を感じているので、現場からの中継は一旦ここまでにしておきます。

AppBrew で働く魅力

LIPS はコスメに特化した SNS です。そのため、LIPS で扱うデータは教科書的なもの*13と比べると、様々な癖があります。例えば、口語表現が多く、感情はしばしば絵文字で表現され、ユーザは構造的な文書を書きません。

これはある意味では困難なわけですが、それと同時に技術者としては面白さ・やりがいを感じるところでもあります。馴染みの薄かった分野・文化を、科学的なアプローチで理解し、ときには社内にいるコスメオタクにヒントを貰いながらアプリを作れることが、この会社でエンジニアリングをやる面白さだと思っています。

私は主に検索機能まわりを改善していて、最近は bosyu の募集から自然言語処理が得意な人が続々と入ってきていて、出来ることが広がりそうでワクワクしています。

bosyu.me

まとめ

この記事では私の具体的な話を通して、AppBrew の魅力や文化を紹介しました。

繰り返しになりますが、AppBrew の組織文化は今も社内で育てられ続けているものです。 より良いものを目指して今も社内でしばしば議論を重ね、試行錯誤している様子が Slack にドバドバ流れています。

特に、社長は AppBrew の羅針盤として強いカリスマ性と「哲学」を持っているので、お話を聞きに来るだけでもきっと面白いと思います!お待ちしております!

*1:スタートアップ企業の面接では取締役と話すことが多いです。彼らは基本的に投資家から出資を受けて会社を作っており、会社の魅力を説明することにおいてはプロフェッショナルです。僕みたいな素人は一瞬で騙されてしまうかもしれません。

*2:前職にも3年間の学生アルバイトを経て入社しました。

*3:会社側から見ても採用リスク・離職リスクを下げられるので、アルバイト期間を挟むメリットがあるはずです。このように説明すれば融通を効かせてくれる、柔軟な会社で働きたいな、というキモチもありました。

*4:たぶん。並列計算からの類推です。

*5:僕はまだ日も理解も浅く、しばしば他の人に助けてもらってますが...。

*6:入って1ヶ月の僕が仕事の方法論とか偉そうに書いているのも資料をチラ見しながら書いてるからです。文書化は正義。

*7:ヤバすぎてボキャブラリがなくなりましたが、ヤバいです。

*8:給与を下げるのは難しいのに対して、賞与を下げるのは難しくない。意思決定を可逆することで「とりあえずやってみる」ことができる。

*9:ご飯を頼むのも早い

*10:そのような行動指針が文書化され、公開されています。

*11:給料から主要KPI、受注状況までなんでも公開されています

*12:AppBrew は東大発のスタートアップで、東大から歩いて通えます

*13:例えば TwitterWikipedia のデータ

使われないアプリを作らない方法

遊撃エンジニアの @anoworl です。普段はバックエンドやインフラの開発を中心に、ライブ動画配信の仕組みをAWS MediaLiveで構築したり年末年初のCM放映に伴う負荷対策をしたり…今は採用や2B向けのSaaSも開発しています。CMに出演したローラさんがオフィスに来たのは良い思い出です。

だんだんと社員が増えて会社っぽくなってきた弊社では採用活動に力を入れているのですが、その中でお話するとウケが良かった話をここでは紹介したいと思います。

それは弊社のミッションである「ユーザが熱狂するプロダクトを再現性をもって創造する」に直結する、チームの文化です。

そこでこの記事では「使われないアプリを作らない」ために私達が愚直にやっている方法を記します。

アプリを作ると一口に言っても「新規に作る場合」「既存のものを改善していく場合」がありますが、ここでは後者「既存のアプリを改善する場合」に焦点を当てて話します。

2つのやっていること

私達が「使われないアプリを作らない」ためにやっていることは大きく2つあります。

  • アプリを改善するとき「ちゃんと使われる」か予め定量的に分析すること
  • 実際に改善した後「ちゃんと使われたか」定量的に確認すること

どちらも当たり前のことではあるのですが、実際に開発フローへ組み込もうとすると一筋縄ではいかなかったりします。

例えば…

  • 現状の分析が行われないまま機能が開発され、結局その機能は使われなかったり…

  • 改善後の分析をしなかったため良くない改善(改悪?)がいつまでもアプリに残ったり…

ここでは私達の方法を共有していきます。よろしければみなさんの方法も教えてください!

なお以下では新機能の開発や既存機能の改修含めて、アプリの 改善 と記します。

改善例

以下はこれから説明する方法にしたがって実際に改善した「人気投稿が表示される画面」の改善例です。

この時は人気投稿のレコメンドロジックを改善することで「この画面からクチコミへの遷移率アップ」や「全体の滞在時間アップ」を達成し、ちゃんと使われる機能を作ることができました。

f:id:appbrew-sota:20190408162047p:plain
人気タブのレコメンドロジック改善(企業秘密は一部伏せ字です)

次の項からはこちらの例を使って、実際にやっていることを説明していきます。

「ちゃんと使われるか」予め分析すること

私達のチームでは改善の起案者が以下のテンプレートを利用してGithubのIssueを作成します(流し見で大丈夫です)。

`*` は作成者or優先度変更者が必ず埋めてください

### ユーザーの抱える問題
- 簡潔に説明*
- スクショ
- Redashリンク

### インパクト想定
- 対象: ヘビーユーザー / 全ユーザー / 他特定ユーザー(ex: 投稿ユーザー)*
- 対象画面のDAUU: *
- 使用頻度: daily / weekly / monthly *
- 改善する数値(Epic): *
- 数値の伸び幅(10倍改善?数%改善?): ◯倍 もしくは ◯%

### 問題の解決策はなにか
- 簡潔に説明*
- モック
- スクショ

### 検証内容
- AB必要か* : yes / no
- 取りたいイベント

---

### 実装
- 各RepositoryのIssue/PRで本Issueを参照すること
- コメントにてRedashの検証用URLを貼る

ここで取り上げたいのは「インパクト想定」の部分です。

私達のチームでは「使われないアプリを作らない」ために、改善の起案時に必ずインパクト想定を行います。

ここで例として「人気タブのレコメンドロジック改善」のIssueを見てみましょう。

### インパクト想定
- 対象: 全ユーザー
- 対象画面のDAUU: ○○ DAUU
- 使用頻度: daily
- 改善する数値(Epic): 24h以内平均閲覧数, 閲覧ユーザRR, おすすめタブ滞在時間
- 数値の伸び幅(10倍改善?数%改善?): 5〜10% 

インパクトの大きさを不確実ではありますが見積もり、その上で開発工数優先順位付けをし開発を行っています。

ユーザの行動ログを分析してインパクトを見積もる

上記の項目を埋めるためには、ユーザの行動ログから任意の軸でデータを取り出す必要があります。

私達はRedshiftにユーザの行動ログを格納しており、それをRedash*1経由でSQLを使い数値を取り出すことで、上記項目を埋めています。

そのため全ての開発者やデザイナーはもちろん、マーケチームやセールスチームもSQLを書きRedash経由でデータを取り出せる環境を作っています。

なお、データを貯めていくアーキテクチャについては以下の記事に詳しくあるので、ご興味のある方はご覧ください。

定量的な認識を揃え、効果の高い所から手を付ける

私達は全員が数字を自分の手で出せるようにすることで、それをベースとして議論を行い、優先順位付けを行っています*2

情報格差をチームメンバー間で作らないことで、定量的なデータを無視して開発が進んでしまうことを防いでいます。

私達はこれらの方法でインパクトを 想定 し、なおかつそれを 活かす 環境を作ることで「使われない」機能を作ってしまうリスクを軽減しています*3

「ちゃんと使われたか」確認すること

私達のチームでは機能をリリースした際に「ちゃんと使われているか」「良い結果を生んでいるか」必ず確認し、もしそうなっていなければ機能の追加を取りやめます。

これによりアプリが改悪されることを防いでいます。ここではその方法について記します。

機能追加時には必ずABテストを行う

私達は機能追加・改善をおこなった際には必ずABテストを行っています。

ユーザをランダムに「新規機能を使える群」「新規機能を使えない群」に分け、それぞれのデータを前述のRedashで分析します。以下が分析例です。

f:id:appbrew-sota:20190402215130p:plain
ABテスト分析例

このグラフは人気タブのCTRの時系列グラフで、1が機能を使える群だったので、この時の新機能は有効でした。このグラフを作成することに加え仮説検定を行い判断をしています。

もちろん悪くなることもあり、その場合は機能を戻したりします。一番多いのは差が見られないときで、その際は定性的な情報などを考慮し、最終的に判断します。

他の人間が検証できるようにする

上記の分析はIssueにRedashのURLを貼ることで、他の人間が追試験できるようにしています。

定量で測ると一口に言っても、計測の方法やデータの解釈は多様で、一人の人間だけで判断するのには限界があります。そこで私達は他のメンバーが「おや?」と思った際には、違う軸で再試験できるようにテスト結果やその過程を共有しています。

これにより一人のメンバーの中で検証や判断が閉じてしまい、見落としや勘違いが起こるリスクを下げています*4

ABテストが終わったことを確認してからIssueをクローズする

人間の心は弱いので上記のことを志しても、なあなあになって結局できないことがあります。

人間の弱さは仕組みでカバーすべきなので、弊社は簡単な解決策ですが、カンバンに「検証中」という列を設けています。

f:id:appbrew-sota:20190402232509p:plain
実装中 → レビュー/QA待ち → 検証中 → Closed

Issueは必ず「検証中」で検証しなければClosedに移せないというルールにしています。なおかつ週次の定例で検証結果を共有するようにしており、否が応でも検証という過程は飛ばせないようにしています(そもそもメンバーは重要性を認識しているので、こっそり飛ばそうという人もいないのですが…)。

こうして人間の弱さをカバーすることで、「使われる」アプリを作ろうと努力しています。

おわりに

こうやって私達は改善前後の検証を定量的に行うことで「使われないアプリを作らない」ように心がけています。

エンジニア的に言うなら「 推測するな、計測せよ 」です。私達はアプリを改善するときも、それを実践しています*5

「使われるアプリ」作っていきましょう!

We are Hiring

AppBrewでは定量・定性に真摯に向き合ってプロダクトを開発したいメンバーを募集しています!

ご興味がある方は転職ドラフトやWantedlyよりぜひご応募ください!

*1:SQLを書くことでDBから数値を取り出し、それをビジュアライズできるダッシュボードツールです。ウェブブラウザで利用できます。Redash公式サイト

*2:もちろん定性も重要視し、ユーザインタビューやアプリレビュー、アプリ内やSNS上のユーザの反応などは誰でもすぐに見られるよう社内QiitaやSlackにまとまっています。

*3:よくあるQ&Aとして「細かな改善だけに終止し部分最適することだけに陥ってしまうのでは?」というものがあります。確かに開発工数が少なく効果が高そうなものを選んでいこうとすると、そのような傾向になりがちです。ただ私達もそれは認識した上で、たとえ時間がかかりそうであっても大きなインパクトが見込めるものは意識的に開発するようにしています。例えばですが最近は「ライブ配信機能」の開発に一ヶ月をかけました。

*4:余談ですが、全社員には人員採用計画や売上計画、全社員の給与・SO、調達の状況なども公開されており、他のメンバーが「おや?」と思った際には確認できる環境が整っています。

*5:とはいえ、まだまだ改善したい点はいくつもあり、RedashはSQLの再利用がコピペになりがちなのでLookerを検討したり、仮説検定の作業に一部手作業が含まれるのでFirebase A/B テストを検討したり、よりよいKPI・KGI・OKRの設定を議論したり…。

スタートアップでも出来る分析基盤

こんにちは、遊撃エンジニア兼代表の深澤です。
最近はインフラからサーバーをメインにいじっています。昔はクライアントも書いていました。

弊社は、「再現性を持ってユーザーに刺さるプロダクトをつくる」ことを目指しチームビルディングをしています。
なので、創業からのてんやわんや(スタートアップは皆そうです)の中で、数字とちゃんと向き合う方法を模索してきました。
結果として、今現在どういった分析基盤で仕事をしているかに関して書きたいと思います。

※注

  • あくまで、2017年初頭にサービスインしたLIPSの分析基盤を、分析について何も知らない人間が組んできたという話です。開始の技術選定からは1年以上経っているので、参考程度にお願いします。
  • 技術的には枯れた内容しかやっていません。分析は、技術だけでなく、掛けるコストやオペレーションに組み込むレベルの話が出来てはじめて意味をなすものなので、そちらの話がメインです。

1. 前提:限られたリソース

AppBrewは、コスメの口コミアプリであるLIPSを運営している会社です。

  • イベントログ数は数千万per dayくらいの規模
  • アプリなので、ログ分析からのファネル改善・リテンション改善が最重要な業務であり、命
  • データ専任をおけるほどのリソースがない状態だった

2. スタートアップの分析基盤に必要なもの

  • 技術(凝りすぎない程度に、とはいえちゃんとわかった上で組みたい
  • 文化(リソース不足の状況だと、こちらが重要かなと思います

3. 技術面:基盤構成

LIPSのサービスはAWS上に載っています。 f:id:appbrew:20180326162227p:plain

特にバックエンドの部分を抜き出すと上のようになっています。 ポイントとしては、

  • EC2の各インスタンス上からfluentdでkinesisに投げる
  • kinesisからfirehoseでredshiftのインスタンスに流し込む
  • kinesisからfirehoseでS3に取り置く(Athenaで引ける(引くとは言っていない
  • kinesisからlambdaを発火して、低レイテンシーでデータ分析用に使いたいデータ・サービスから改めて使いたいデータ(クリックログなど)をDynamoDBに流し込む
  • Redash(Sassバージョン)を使い、Redshiftをメインのバックエンドとして利用する
  • Redashは非エンジニア含む社員全員に公開している

他に蛇足として

  • メインのRDSから必要なテーブルをdata pipelineでredshiftにdumpしているが、管理が面倒・タスクのトレーサビリティが低いので直近でembulkを利用する計画
  • firebaseを利用しているので、BigQueryに流し込んではいる(あまり使えていない

というような構成になっています。 個別に説明すると、

Redshiftを利用している理由

  • 個人的に慣れがあった
  • サーバー立てる形式なので、高いとはいえ価格が見積もりやすく、皆が触れるBIバックエンドとしては安心感(クエリ課金は怖い
  • 標準のpsqlがほぼ使えるので、導入しやすい(BigQueryがstandardSQLに対応した前後がサービスインのタイミングでした

Redshiftのデメリット

  • スキーマを自前できっちり組まないといけない
  • インデックスを適切に貼ったうえで、vacuumを定期的に回さないといけない(CIでやってます

なので、わかる人一人しかいない状態などであれば、firebaseからBigQueryに流し込んでいるものをそのまま使うなどがいいかもしれません。
スピードに関しては、インスタンスサイズ・スキーマやクエリ・インデックスの状態によって違い、完全にどちらが早いというようなデータは見つからなかったので言及は避けます。
参考データは以下

aws.amazon.com

www.periscopedata.com

Dynamoを利用している理由

  • サービス側から引きたいデータはredshift上ではスケーラビリティ・レスポンスタイムの関係で無理がある(当然
  • クリックログやベクトルのようなデータだと、RDBMSはあまり向いてはいないし、メインのDB利用は密結合になるので嫌だった
  • 永続化したいのでオンメモリのKVS等にのせるのも不安があった
  • AWSを使っているので、Dynamoが選択肢に上がった

また、クリックログのようなデータであれば、スケーラビリティや速度(ある程度)は求められますが、ImmutableでConsistencyは不要なのでDynamoを導入しました。 ここからPythonの分析用インスタンスでデータを引いて、ごにょっています。

以下のリンクもかなり参考にしました。

tech.gunosy.io

とはいえ、そもそもスタートアップであれば普通はここまでやらなくていいかと思います。

4. 文化面:皆で分析したい

スタートアップのあるあるとして

  • 肝心なところにログを仕込んでいない→欲しい時に数値取れない
  • 一人に分析タスクが集中して、ボトルネック化・つらい
  • リリースフローが雑なのですぐログが壊れる

三番目はちゃんとやれって話なのですが、上2つは、データに皆で触れる文化形成の問題が大きいのかなと思います。
AppBrewでは、意識を高めるために、オープン&フラットな組織に加え、皆でクエリを書くようなRedash活用を心がけています。

Redash活用法

  • 皆でログ仕込む・クエリ書く(エンジニアは5人、クエリを書く人は7人 全員が毎日書いているわけではないですが、ビズサイド含めかなりの人数がRedashで書く側に回っています
  • slackに垂れ流す(毎日Redash見にいく習慣がないとなかなか活用出来ないので、slackで特定のチャンネルにKPI定期で貼っています
  • issueにもredashのクエリをちゃんと貼る(まだ始めたばかりの習慣ですが、定性的な内容も出来る限り数字で裏を取ろうとします
  • ちゃんと改善につなげる(CTRを上げたり、アクション率を上げたり、ちゃんと数字につなげるように業務自体を進めます
  • 失敗した施策は取り下げる(リリース後も数字を確認して、失敗したらもとに戻すことを心がけます

※画像はイメージです

f:id:appbrew:20180326183532j:plain

全部当たり前のことなのですが、全部が普段の業務レベルで染み付いていないとなかなか定着しないのも事実だと思います。

弊社もまだまだ改善中なので、こんな事例あるよというのがあれば、是非教えてください!

5. 最後に

今回は、AppBrewではベンチャーなりにちゃんとデータを活用していますという話を紹介しました。

このようにデータの下で仕事をすることで、再現性を持つ新規立ち上げプロセスを作ることを目標にしており、新規事業等も計画しています。

もしご興味がある方がいらっしゃったら是非私のtwitter
Yuta Fukazawa (@YFuka86) | Twitter
か、以下のwantedly(データ・サーバエンジニアの募集書いてませんでした)までご気軽にお願いします!

www.wantedly.com

RailsプロジェクトでのElasticsearchとの付き合い方について

こんにちは、AppBrewのエンジニアの吉野です。 前回の記事ではElasticsearchとはどういうものか、ということについて書きました。

tech.appbrew.io

この記事では、

①ローカルでのElasticsearchの環境構築

から、

Railsとどうつなぎ合わせるか

③実運用上での注意点など

について書いていきたいと思います。

動作環境

Rails 4.2.8 Elasticsearch 2.4.5

①ローカルでのElasticsearchの環境設定

開発するにあたり、ローカルで環境がほしいと思います。 ElasticSearchのAnalyzerは標準としていくつかありますが、日本語のTokenizeなどをするならば Analyzer として、kuromoji Analyzer、icu_normalizer あたりを入れていきましょう。 GUIはelasticsearch-headを入れておくことにします。

brew install elasticsearch@2.4
echo 'export PATH="/usr/local/opt/elasticsearch@2.4/libexec/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
plugin install analysis-kuromoji analysis-icu mobz/elasticsearch-head

次に設定を多少いじりましょう /usr/local/opt/elasticsearch@2.4/libexec/config/elasticsearch.ymlに以下の行を追加しておくと何かと便利です。

script.inline: on
script.indexed: on
script.engine.groovy.inline.search: on
script.search: on
script.groovy.sandbox.enabled: true

起動は elasticsearchで可能です。

$ elasticsearch
[2018-02-25 16:53:50,683][INFO ][node                     ] [Mandroid] version[2.4.6], pid[1939], build[5376dca/2017-07-18T12:17:44Z]
[2018-02-25 16:53:50,683][INFO ][node                     ] [Mandroid] initializing ...
[2018-02-25 16:53:51,358][INFO ][plugins                  ] [Mandroid] modules [reindex, lang-expression, lang-groovy], plugins [head, analysis-kuromoji, analysis-icu], sites [head]
[2018-02-25 16:53:51,420][INFO ][env                      ] [Mandroid] using [1] data paths, mounts [[/ (/dev/disk1)]], net usable_space [40.6gb], net total_space [232.5gb], spins? [unknown], types [hfs]
[2018-02-25 16:53:51,420][INFO ][env                      ] [Mandroid] heap size [990.7mb], compressed ordinary object pointers [true]
[2018-02-25 16:53:51,420][WARN ][env                      ] [Mandroid] max file descriptors [10240] for elasticsearch process likely too low, consider increasing to at least [65536]
[2018-02-25 16:53:53,126][INFO ][node                     ] [Mandroid] initialized
[2018-02-25 16:53:53,126][INFO ][node                     ] [Mandroid] starting ...
[2018-02-25 16:53:53,213][INFO ][transport                ] [Mandroid] publish_address {127.0.0.1:9300}, bound_addresses {[fe80::1]:9300}, {[::1]:9300}, {127.0.0.1:9300}
[2018-02-25 16:53:53,219][INFO ][discovery                ] [Mandroid] elasticsearch_yosshi/yI1KRtR_TGCC34vpmzoJEQ
[2018-02-25 16:53:56,268][INFO ][cluster.service          ] [Mandroid] new_master {Mandroid}{yI1KRtR_TGCC34vpmzoJEQ}{127.0.0.1}{127.0.0.1:9300}, reason: zen-disco-join(elected_as_master, [0] joins received)
[2018-02-25 16:53:56,281][INFO ][http                     ] [Mandroid] publish_address {127.0.0.1:9200}, bound_addresses {[fe80::1]:9200}, {[::1]:9200}, {127.0.0.1:9200}
[2018-02-25 16:53:56,281][INFO ][node                     ] [Mandroid] started
[2018-02-25 16:53:56,363][INFO ][gateway                  ] [Mandroid] recovered [6] indices into cluster_state
[2018-02-25 16:53:57,592][INFO ][cluster.routing.allocation] [Mandroid] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[post_development_3][1]] ...]).

デフォルトはlocalhostの9200番でサーバーが立ちます。 また、 http://localhost:9200/_plugin/head/ へとアクセスすると香ばしいGUIでElasticsearchの中にあるデータを確認できます。

f:id:yosshi0774:20180314000102p:plain

ローカルの設定は以上です。

Railsとどうつなぎ合わせるか

ここからRails側でElasticsearchを使うための設定について書いていきます。

自前で必要なAPIを叩くようなものをwrapしていっても良いわけですが、 便利なものが世の中にはあるので使っていきましょう。 今回使うGemは以下の4つです。

gem 'elasticsearch', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git'
gem 'elasticsearch-dsl', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git'
gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'

それぞれのgemは以下のような機能を提供してくれます。

  • elasticsearch
    • ElasticsearchのrubyのAPIClientを提供してくれています。
    • 通信部分で気になることがあればこれを見ましょう
  • elasticsearch-dsl
    • Elasticsearchのmappingの定義、検索のクエリ設計などをrubyっぽく書くことができるようになります。
    • このライブラリ中を見てみると検索クエリの書き方のサンプルが多くのっているのでわからなくなったら見てみましょう。
  • elasticsearch-model
    • 名前の通りincludeすることでそのmodelに対してsearchやimport などElasticsearchに関連したメソッドを生やしてくれるものです
  • elasticsearch-rails
    • development環境でelasticsearch関連のlogをキレイに吐いてくれたり、データ投入のためのrake taskなどを提供してくれます。

必要なgemは入れたので実装に移りましょう。

Searchable moduleを実装

mappingは各modelによって違うとして、いくつか共通化したい処理や設定が存在するかと思います。 そのため、searchable.rbのようなmoduleを作り、共通化していきましょう。

searchable moduleでは2つの役割を担っています

①カスタムしたanalyzerの設定 ②レコードに関するコールバックの設定 ③APIクライアントの初期化

具体的には以下のようなものとなっています。

// 'app/models/concerns/searchable.rb'
require 'elasticsearch/model'

Elasticsearch::Model.client = Elasticsearch::Client.new log: true, scheme: 'https', host: , port: 'ポート番号', user: 'user名', password: 'password'

module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    after_commit on: [:create] do
      ElasticSearchIndexerJob.perform_later(self.class.to_s, self.id) unless Rails.env.development?
    end

    after_commit on: [:update] do
      ElasticSearchIndexUpdaterJob.perform_later(self.class.to_s, self.id) unless Rails.env.development?
    end

    after_commit on: [:destroy] do
      begin
        __elasticsearch__.delete_document unless Rails.env.development?
      rescue => ex
      end
    end
     
   settings analysis: {
      analyzer: {
        ....
      }
    }
  end
end

本来createやupdateはbulkで行うべきなのでしょうが、軽い実装なら上記のようなコールバックを用意しておき、以下のようなjobを用意してあげれば、dbとelasticsearchの同期をとることができます。

class ElasticSearchIndexerJob < ActiveJob::Base
  queue_as :default

  def perform(klass, id)
    sleep(1)
    record = klass.constantize.find_by(id: id)
    record.__elasticsearch__.index_document if record.present?
  end
end

各modelの設定

moduleを作ったため、これをincludeして、各モデルの設定をすれば大丈夫です。 やることは以下4点です。

①indexの定義 ②mappingの定義 ③as_indexed_jsonのオーバーライド ④search queryの設計

まずサンプルコードを貼ると以下のような形となります。

class Post < ActiveRecord::Base
  include Searchable

 index_name "post" #①

  mappings dynamic: 'false' do  #②
    indexes :content, analyzer: 'kuromoji', type: 'string'
    indexes :n_content, analyzer: 'ngram_analyzer', type: 'string'

    indexes :like_count, type: 'integer'
    indexes :media_status, type: 'integer'
    indexes :published_at, type: 'date'

    indexes :info, analyzer: 'ngram_analyzer', type: 'string'

    indexes :products, type: 'nested' do
      indexes :name, analyzer: 'ngram_analyzer', type: 'string'
      indexes :id, type: 'integer'
    end
    ...
  end

  def as_indexed_json(option = {}) #③
     info = "hogehoge"
     attributes
      .symbolize_keys
      .slice(:content, :like_count, :published_at)
      .merge(n_content: self.content)
      .merge(media_status: media_status)
      .merge(info: info)
      .merge(products: products.map{|p| {id: p.id, name: p.name}})
      ...
  end

  has_many :products, through: :post_products

  def self.search(params) #④
    search_definition = Elasticsearch::DSL::Search.search {
      from (page.to_i - 1) * 20
      size 20
      query {
        function_score {
          query {
            filtered {
              query {
                multi_match {
                  query text
                  operator 'and'
                  fields %w{ content n_content info } 
                 }
              }
           }
         ...
    }   
    __elasticsearch__.search(search_definition)
  end
end

それぞれについて見ていきましょう

indexの定義

これはわざわざ説明するまでのこともなくindexの名前を設定しています。 バージョニングなどを考えたindex名については後述しますが、基本的にはわかりやすい名前をつけておけば大丈夫です。

mappingの定義

どのfiledがどういう型か、どういったanalyzerを使用するかということを定義しています。 RDBでいうところの1対多の関係を表すときは type: 'nested'をつけておくと配列をindexingすることができます。

as_indexed_jsonのオーバーライド

mappingに則り、indexingするデータをhashの形へと変換してあげます。 ここで不足があると mappingは定義したのにデータが投入されない ということが起こってしまうので気をつけましょう

search queryの設計

最後にmappingをもとに検索クエリを組み立てて行きます。 本当に多くのクエリを用いて検索ができるため、詳細は省きますが、elasticsearch-dslのgithubを見てみると多くのサンプルを見ることができます。

これでsearchクエリさえ組み立てることができれば大丈夫というところまできました。 mappingの定義が大丈夫となったら (Model名).__elasticsearch__.import をしてデータを投入してみましょう。

これでいつでも検索できるようになりました!

③実運用上の問題点と解決法

最後に運用上でやっていることや問題となったことについてまとめたいと思います。

いまいちどの辺がクエリにヒットしてレコードが引っ張られているのかわからない問題

我々がやったことといえば、mappingを決め、要件をJSONに落としこんだだけですが、 実際与えらえれたクエリに対してどの辺がヒットしているのか? ということを確認したいニーズは多少あるかと思います。 そんなときは highlight という機能を使うと幸せになれるかと思います。

上記self.search(params)のクエリ定義の中に

  def self.search(params) 
    search_definition = Elasticsearch::DSL::Search.search {
      from (page.to_i - 1) * 20
      size 20
      highlight {                                       
        fileds: [:n_content, :content]          # ここを追加
      }                                                        
      query {
        function_score {
          query {
       ...

highlightというブロックを追加します。 そして検索して結果をほっていくと

 pry(main)> Post.search({text: "キャンメイク"}).response.hits.hits.map(&:highlight)
 => {"n_content"=>
   ["<em>キャンメイク春のコスメ</em>♡\n" + "可愛<em>い</em>過ぎ♪\n" + "限定色とか新色とか新作が出るそうです。\n" + "ちなみにTwitter情報です!\n" + "新作は色<em>ん</em>なカラーが出ますね。\n" + "すご<em>く</em>楽しみです!\n" + "セザ<em>ンヌの新色も楽しみで欲しいのい</em>っぱ<em>い</em>w"],
  "content"=>["<em>キャンメイク</em>春のコスメ♡\n" + "可愛い過ぎ♪\n" + "限定色とか新色とか新作が出るそうです。\n" + "ちなみにTwitter情報です!\n" + "新作は色んなカラーが出ますね。\n" + "すごく楽しみです!\n" + "セザンヌの新色も楽しみで欲しいのいっぱいw"]

となり、ひっかかった部分が強調タグで囲まれていることがわかりますね。 "キャンメイク"と打ったのに”ク”だけに反応していたりして、なかなか難しそうですね。

引っ張ってきたレコード、スコア順じゃない問題

elasticsearch-modelの恩恵は偉大で (Model名).search(params).records.to_aとするとSQLを発行し、レコードを取得できるわけですが、クエリを見てみると

  Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (301306, 300359, 297179, 304908, 302542, 299076, 312292, 303429, 308305, 304312, 311940, 297988, 301020, 307750, 305654, 298394, 311388, 308077, 297068, 296636)

のようにwhereで引いてきているため、elasticsearchでのスコア順が保証してくれません。

そのため自前でソートするのがよいかと思います。

(例)

Post.search(params).map(&:id).tap { |ids| Post.where(id: ids).order("field(posts.id, #{ids.join(',')})") }

で順番が保証されます。

activerecord-importとelasticsearch-modelのimportがconflictする問題

(※導入しようとした当時はconflictしてたのですが、現在ではactiverecord-importのimportメソッドはbulk_importにrenameしたようです。 参考: https://github.com/zdennis/activerecord-import/blob/master/lib/activerecord-import/import.rb)

一応困ってる人がいたりするかもしれないので軽く残しておくと elasticsearch-modelが各モデルに対して生やしてくれるメソッドとactiverecord-importが ActiveRecord::Associations::CollectionAssociation に生やしてくれるimportメソッドが被ってしまうという問題があり、メソッドの探索順序的に elasticsearch-modelが生やしてくれるimportが常に使われてしまうという問題がありました。

これはconfig/application.rb

require 'activerecord-import/base'

class ActiveRecord::Base
  class << self
    alias :bulk_insert :import
    remove_method :import
  end
end

としてaliasを貼ってあげることで 解決していました

fieldを追加したりmappingを変えたくなったんだけどサービスはとめたくない問題

ベストプラクティスかどうかはわかりませんが、 弊社ではindex_nameにsuffixをつけることでblue-greenの切り替えをを行っています。

具体的にはconfig/initializersに以下のような定数を記述したファイルをつくり、

  CURRENT_ES_POST_INDEX = 2
  CURRENT_ES_PRODUCT_INDEX = 1
  CURRENT_ES_USER_INDEX = 2
  CURRENT_ES_ARTICLE_INDEX = 0

各modelのindex_nameを

class Post < ActiveRecord::Base
  include Searchable
  index_name "post_#{Rails.env.production? ? 'production' : 'staging'}_#{CURRENT_ES_POST_INDEX}"
  ...
end

のようにしておくと

①mappingを定義しなおしたものをデプロイ ②rake taskなどでCURRENT_HOGEHOGE_INDEX をインクリメントしたインデックスをつくりデータを投入

Post.__elasticsearch__.index_name = "post_#{Rails.env.production? ? 'production' : 'staging'}_#{CURRENT_ES_POST_INDEX + 1}"
Post.__elasticsearch__.import

③作り終えたら実際にconfig/initializersの下にある定数をインクリメントしデプロイ

というフローでblue-greenを実現することができます。

検索候補とか出したい

検索システムを作ってると候補を出したいってなることがあるかと思います。

書き始めると長くなるので、とても参考になるURLを貼っておきます。

https://medium.com/hello-elasticsearch/elasticsearch-%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E3%82%B5%E3%82%B8%E3%82%A7%E3%82%B9%E3%83%88%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E8%A8%AD%E8%A8%88-352a230030dd

基本的には,弊社では検索のログや、ブランド名、商品名、ユーザー名などを、(とても参考になるURLで言われているような)Analyzerを用いてtokenizeするだけでそこそこのものができたりします。

try! Swift 2018参加レポート

みなさんこんにちは。Androidアプリエンジニアの岡田です。 2018年3/1~3/3に行われたtry! Swiftに参加して来ました。try! Swiftは海外の方も多数登壇数する大きなカンファレンスで、東京で開催されるのは3回目です。 たくさんあったトークの中で気になったトークをいくつか紹介します。

Optimizing Swift code for separation of concerns and simplicity

Swiftのコードをより簡潔に書くためのテクニック、Tipsの紹介でした。 変数を関数に切り出す例やprotocol extensionを使用する例では実装の意図が伝わりやすくなるという意味で参考になりました。他の例ではUIStackViewなどのUIKitを使った物があるので、iOSアプリ開発で活かせる場面が多々あると思います。 個人的にはExample10の印象が強かったです。ローディング中、ローディング完了、ロード失敗の状態を管理する方法なのですが、enumのAssociated Valueを使って表示したいViewを保持しています。enumの特性が上手く活用されていて見習いたい実装例でした。 よりSwiftyなコードを書くために紹介されていた方法を取り入れていきたいです。

Kotlin for Swift Developers

Swiftとの比較をベースにKotlinの紹介をするというトークでした。 始めにKotlinとSwiftの似ている点についていくつか述べられていました。 次にSwiftのgurad letをkotlinに,kotlinのapplyをswitで使えるようになると便利なのではないかという提案があり、納得感のある提案だなと思いました。普段両言語を書いている方はそう思われる方が多いのではないでしょうか。 またkotlin nativeについての紹介もあり、現在はプロダクションで導入するのはリスクを取る必要があるとの事でした。 kotlinを学んでAndroidアプリ開発にもtryしていきたいですね。

Preparing for Swift 5 Ownership

Swift5で導入予定のOwnershipについてのトークでした。 新しく導入される要素にsharedキーワードがあり、これを関数の引数につける事でreadonlyの参照渡しになります。このようにコピーと参照を制御するキーワードを使用する事でメモリ管理をより詳細に行うことができます。他にmovedキーワードもあり、これらを上手く使いこなせばパフォーマンス向上だけでなく、実装の意図が分かりやすくなります。 こういった要素が増えると導入障壁が上がるのかと思いますがopt inなので機能を使用しないことも可能なようです。 Swif5リリースまでに導入される概念や機能について理解し、スムーズに以降したいです。

UIImageView vs Metal

GPUがどんな物なのかという導入からMetalを使った自作ライブラリのパフォーマンス測定、その過程で得られた知見についてのトークでした。 MetalとGPUについては知識は殆どなかったのですが、GPUとCPUでのコマンドがどのようにやり取りされているか紹介されていたので処理の概要について知ることができました。 自作ライブラリのパフォーマンス測定ではInstrumentsを使用したデバッグの過程が詳細に述べられており、非常に興味深い内容でした。個人的にはInstrumentsを上手く使いこなせていなかったので、その使用例としても面白い内容でした。 iOSアプリは画像描画の速度でユーザー体験が大きく変わるのでパフォーマンス改善をする際にはMetalの部分まで考慮して実装していきたいです。

会場の様子

IMG_3976 2.JPG

IMG_7156.JPG

感想

3回目の参加になるのですが今回もSwiftに関する様々な発表を聞く事ができ、非常に貴重な経験になりました。 懇親会も盛り上がっており、日本の方だけでなく海外の方ともSwiftについて話すことができました。 とても素晴らしいカンファレンスなので来年以降もぜひ参加したいです。