175
103

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【個人開発】飲み方やおつまみの相性から直感的にウイスキーを探せるサービスを作った話🥃

Last updated at Posted at 2022-07-09

OGP.png

飲み方やおつまみの相性、香りの強さ、口当たりの軽さなど
直感的な要素からウイスキーを探せるサービスをリリースしました!🥃

▼サービスURL
https://malt-mate.jp

▼GitHub
https://github.com/tarakish/whiskey_database

はじめに🧎🏻

はじめまして!駆け出しへべれけエンジニアのみやと申します🫠
ウイスキーについても駆け出しのためネットで情報を探すことが多いのですが、
フレーバー?歴史?タイヤのような香り?カスクストレングス?etc...前提知識が必要だったり、
そもそもの情報量が多すぎて分からなくなることが多いな〜と感じてました。そこで

  • 相性のいい飲み方やおつまみなど、飲む際に実際に考えるような要素から探せる
  • 網羅するのではなく、逆に情報を絞って表示する
  • 絞った上で表示する情報は直感的に分かりやすくする

そんなサービスがあれば、同じような悩みを持つウイスキービギナーのウイスキー選びを助けられるんじゃないか?
あわよくば、沼に引っ張りこんで飲み仲間ができるんじゃないか?
そんな 企み 想いから本サービスの開発に思い至りました!🥃

サービス概要と使い方🥃

コア機能(ウイスキーの詳細・検索機能)🔎

各ウイスキーに対して、相性のいい飲み方やおつまみを紐付けていることでその要素から検索ができます。

一覧ページから検索 ウイスキー詳細での表示
一覧で検索 詳細でのペアリング
一覧からは利用頻度の高い「飲み方」「おつまみ」での絞り込みのみに留め、認知負荷を下げる。 「相性のいい飲み方」「相性のいいおつまみ」をクリックすると各一覧ページの対象箇所に遷移します。

一覧ページではセレクトボックスによる並び替えも利用できます。これをRansackで実現するのに少し手間だったので、以下の記事にまとめました。


その他にも香りの強さ、口当たりの重さ、フレーバー、手に入り易さ、価格などの情報を持たせており、それらの要素からも検索ができます。

詳細検索画面 ウイスキー詳細での表示
詳細検索 ウイスキー詳細での表示
ラベルにチェックボックスを貼り、デザインの統一感と操作性を両立。 一部情報はあえて初期表示しないことで情報量を減らし、認知負荷を軽減。

また、各ウイスキーに紐付けた「おすすめの飲み方」「フレーバー」から、独自メソッドを通して類似度の高いウイスキーを『似ているウイスキー』として表示することで、好みのウイスキーに傾向の近いものを探せます。

ザ・マッカラン シェリーオーク12年の場合

ユーザー機能👤

ユーザー登録することで「気になる!(いいね)機能」「テイスティングノート(投稿)機能」が利用可能になります。
登録&ログインは、Twitter・Google・LINEの外部認証から選択できます。

マイページ 気になる!一覧(マイページ)
マイページ 気になる一覧
テイスティングノート一覧(マイページ) テイスティングノート投稿・編集(ウイスキー詳細)
テイスティングノート一覧(マイページ) テイスティングノート投稿(ウイスキー詳細)

工夫点🤔

本サービスの開発にあたって意識した点、工夫点について、5点ほどお話させてください。

  1. ウイスキーの分かりにくさをどう解消するか
  2. UI/UXのFBをいただくためにノーコードでチャットボットを作成・配置
  3. エラーページのハンドリング
  4. 外部認証のみのログイン機能
  5. 独自メソッドについて

1. ウイスキーの分かりにくさをどう解消するか?

具体的にははじめにで書いた通りですが、抽象化すると主に

  • UI/UXを最適化すること
  • 情報量を絞り認知負荷を下げること(コア機能で説明)

の2軸で解消を試みました。
UI/UXの最適化に関しては、以下のようなことを意識しました。

  • どういった探し方をするだろうか?を考える
    初心者の場合ウイスキーの知識に乏しいため、身近な要素から探すだろうという仮説を立て、「相性のいい飲み方」「相性のいいおつまみ」を軸にすることでより現実に即して検索できるような導線で実装を行いました。

  • FBを参考にする
    「好みのウイスキーに似たウイスキーがわかると嬉しい」とのユーザーの声をいただいたため、独自メソッドを作成し、各ウイスキーに似たウイスキーをサジェストする機能を実装しました。

2. UI/UXのフィードバックを得るため、ノーコードでチャットボットを作成・配置

上記のような仮説が正しいのか計測をするために、Collect Chatというノーコードツールでチャットボットを作成・配置しフィードバックを集めました。
これにより仮説ではなく、実際に課題解決できていることが確認できました。

3. エラーページのハンドリング

404,500エラーに関しては、サービスで紹介しているウイスキーを表示し、離脱率を下げることを意識しました。

404エラー
404

4. 外部認証のみのログイン機能

本サービスの登録、ログイン機能は、Twitter・Google・LINEの外部認証(Oauth2.0)のみの実装です。
理由としては、主に3点です。

  1. 登録・ログイン時の手間を減らすため
  2. メアド&パスワード認証によるセキュリティ管理コストを減らすため
  3. 外部認証機能の学習のため

1に関しては、UI/UXの観点から。
2、3に関しては、自分がまだ未経験エンジニアであるため。
また、3のアウトプットとして記事を書きました。(sorcery × GoogleLINE認証の記事が見当たらなかったので)

5. 独自メソッドについて

各ウイスキーに似ているウイスキーを算出するために独自でメソッドを定義しました。
冗長かつパフォーマンス上の問題もありますが以下のような形です。(必要な部分のみ記載)

whiskey.rb
class Whiskey < ApplicationRecord
  belongs_to :drink_way
  has_many :whiskey_flavors, dependent: :destroy
  has_many :flavors, through: :whiskey_flavors

  # ウイスキーとフレーバーはN:Nだが、一つのウイスキーに紐付けられるフレーバーは3つまで。
  validates :whiskey_flavors,
            length: { maximum: 3,
                      message:
                      I18n.t('activerecord.errors.messages.upto_three') }


  def similar_to_self
    whiskeys_same_drink_way = Whiskey.where(drink_way_id: drink_way).where.not(id: id)
                                     .eager_load(:flavors, :whiskey_flavors)
    return if whiskeys_same_drink_way.blank?

    original_flavors = flavors.map(&:category_before_type_cast)
    whiskeys_with_similarity = []
    whiskeys_same_drink_way.each do |whiskey_same_drink_way|
      similarity = 0
      comparison_flavors = whiskey_same_drink_way.flavors.map(&:category_before_type_cast).tally
      original_flavors.uniq.each do |i|
        similarity += comparison_flavors[i] unless comparison_flavors[i].nil?
      end
      whiskeys_with_similarity.push({ whiskey: whiskey_same_drink_way, sim: similarity })
    end
    max_similarity = whiskeys_with_similarity.max_by { |i| i[:sim] }[:sim]
    return if max_similarity.zero?

    whiskeys_with_similarity.filter { |i| i[:sim] == max_similarity }.sample[:whiskey]
  end
end
flaovr.rb
class Flavor < ApplicationRecord
  has_many :whiskey_flavors
  has_many :whiskeys, through: :whiskey_flavors

  enum category: { woody: 0, winy: 1, fruity: 2, floral: 3, sereal: 4, smoky: 5 }
end
whiskeys_controller.rb
class WhiskeysController < ApplicationController
  def show
    @whiskey = Whiskey.find(params[:id])
    # ここで独自メソッドを呼び出し、nilの場合はviewで制御しています。
    @recommended_whiskey = @whiskey.similar_to_self
  end
end
similar_to_selfメソッドについて細かく説明します。
whiskeys_same_drink_way = Whiskey.where(drink_way_id: drink_way).where.not(id: id)
                                     .eager_load(:flavors, :whiskey_flavors)
return if whiskeys_same_drink_way.blank?

まず、おすすめの飲み方が同じウイスキー(以後「子ウイスキー」)のコレクション(自身は除く)を取得。
なかった場合はnilを返します。

original_flavors = flavors.map(&:category_before_type_cast)

レシーバのウイスキーインスタンス(以後「親ウイスキー」)に紐付けられているフレーバーのカテゴリー(enum)を数値で取得。

whiskeys_with_similarity = []
whiskeys_same_drink_way.each do |whiskey_same_drink_way|
  similarity = 0
  comparison_flavors = whiskey_same_drink_way.flavors.map(&:category_before_type_cast).tally

各子ウイスキーのフレーバーのカテゴリーの値も同様に取得→tallyでハッシュ化し、comparison_flavorsに入れる。

  original_flavors.uniq.each do |i|
    similarity += comparison_flavors[i] unless comparison_flavors[i].nil?
  end
  whiskeys_with_similarity.push({ whiskey: whiskey_same_drink_way, sim: similarity })
end

親ウイスキーのフレーバーのカテゴリーと、子ウイスキーのフレーバーのカテゴリーを比較し、類似度similarity0~3の範囲で入れていく。
そして「算出した類似度」と「子ウイスキーのインスタンス」をセットのハッシュとして、whiskeys_with_similarityに入れていく。

【補足】
子ウイスキーのフレーバーにtallyと親ウイスキーのフレーバーにuniqを使う理由は、
もし使わなかった場合に

  1. 親のフレーバーのカテゴリーの配列が[1, 2, 3]、子も同様[1, 2, 3]だった場合に類似度は3
  2. 親が[1, 2, 2]、子が[2, 2, 3]の場合に類似度は4

2より1の方が類似度が高いはずが、類似度として逆になるため正確ではなくなってしまうため。
※配列同士のままでうまく算出できればそうしたかったのですが、自分の頭では思いつかず…

max_similarity = whiskeys_with_similarity.max_by { |i| i[:sim] }[:sim]
return if max_similarity.zero?

算出した中から類似度の最大値を取得する。
類似度の最大値が0の場合、類似していないも同義のためnilを返す。

whiskeys_with_similarity.filter { |i| i[:sim] == max_similarity }.sample[:whiskey]

whiskeys_with_similarityから類似度が最大のものをすべて取得し、その中から一つをランダムで返す。

主な使用技術💻

  • Ruby 3.0.1
  • Ruby on Rails 6.1.4
    • 【認証】sorcery & Oauth 2.0(Google・Twitter・LINE)
  • Heroku
  • AWS S3
  • Semantic UI

ER図🥞

erd.drawio.png

おわりに🙇🏻‍♂️

長文にも関わらず最後までご覧いただきありがとうございました!!!
ウイスキーが気になっていた方はもちろん、興味なかったけど記事を読んでちょっと興味が出てきたあなたも
その一歩がウイスキー沼の入り口とも気付かずに ぜひ一度覗きに来てくださると嬉しいです!🥃

▼ Malt Mate
https://malt-mate.jp

▼ Twitter
@malt_mate(サービスの公式アカウント) / @tarakish_23(開発者)

このサービスを開発する中で執筆した記事

175
103
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
175
103

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?