34
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】動物好き必見!動物とのふれあいに特化した検索サービス『FLUFF MAPS』を作りました!

Last updated at Posted at 2022-11-16

top3.jpg

この度「動物とふれあうことができる施設を簡単に検索できるサービス【FLUFF MAPS】」をリリースしました!

サービスURL (現在一時的にクローズしています)
https://www.fluff-maps.com

GitHub
https://github.com/kazuki1025okumura/fluff_navi

はじめに

はじめまして!奥村一貴と申します。
私自身、動物が大好きでよく牧場や動物園にふれあいに行きます。

ネットでそういった施設を探していて、「条件を絞り込んで検索できたらな」だったり「ふれあうことができる動物が一覧で見れたらな」と感じていて、それなら自分で作ってしまおう!と思い製作しました。

このアプリへの思い

長いですが思いを書きました!
サービス作成開始前に「動物とふれあうのは好きですか?」というアンケートを行ったのですが70%以上の方が「好き」または「大好き」という回答でした。「どちらかといえば好きと」いう回答を含めると90%以上になります。 この結果からもやはり多くの方は動物が好きなことを再認識しました。

しかし動物関連の産業には「飼えなくなり捨てられるペットの問題」だったり、「動物のストレスを考慮しない動物カフェ」など、悲しい現実があります。

本サービスでは、施設情報や評判を細かくチェックした上で掲載したり、施設詳細ページに報告機能 をつけるなどの工夫をして動物に配慮が足りない施設をなるべく掲載しない努力をしています。

さらに 保護猫カフェ など、動物の保護活動を行なっていたり、動物の里親になることができる施設を特に力を入れて掲載しています。

このアプリを通じて1匹でも多くの保護動物が新しい家族に出会えたり、小さなお子さんが動物の可愛さや尊さを学べる機会を作ることができたら幸いです。

サービス概要、使い方

1、 条件を指定して検索する

カテゴリーと都道府県(現在東京都と神奈川のみ)を選択して検索します。

  • 検索結果をマップ上で見れます
  • 検索結果一覧でふれあえる動物の種類が分かります
    Image from Gyazo

2、フリーワードで検索する

入力フォームにキーワードを入力して検索します

  • 施設名や地名など複数ワードを指定できます
  • スペースは半角、全角どちらも対応しています
    Image from Gyazo

3、施設の詳細情報を見る

  • 詳細情報や、ふれあえる動物の種類を見れます
  • ログインすることで施設の報告をすることができたり、施設の写真を投稿することができます
    Image from Gyazo

4、カテゴリー、動物の種類を選択して該当する施設を一覧で表示する

Image from Gyazo

こだわりポイントや苦労したこと

1、ユーザーのお気に入りの情報を元にオススメを表示するロジック


プロフィール編集画面で設定したお気に入りの動物やカテゴリーを元にトップページにオススメの施設を紹介する機能を作成しました。

コードの書き方や処理の方法にかなり悩みました。


# ユーザーのお気に入りの動物のidを元に関連する施設をすべて取得する
scope :favo_animal, ->(animal) { Facility.joins(:managements).where('animal_id = ?', animal.id) }
scope :favo_category, ->(category) { Facility.joins(:facility_categories).where('category_id = ?', category.id) }

# 実際にビューやコントローラで使うメソッド
def suggest_to_login_user(user)
  animal = user.animal
  category = user.category
  recommend_data(animal, category)
end

# オススメの施設を選定するロジック
def recommend_data(animal, category)
  facilities = Facility.all
  if !animal && !category
    facilities.where(suggest: 1).includes(%i[facility_categories categories managements animals]).sample(3)
  else
    suggest_animal_facilities = animal ? facilities.favo_animal(animal).to_a : []
    suggest_category_facilities = category ? facilities.favo_category(category).to_a : []
    suggest_animal_facilities.concat(suggest_category_facilities).uniq.sample(3)
  end
end

  • まず最上部のscopeは動物やカテゴリーのidを元に、関連する施設をすべて取得します
scope :favo_animal, ->(animal) { Facility.joins(:managements).where('animal_id = ?', animal.id) }
scope :favo_category, ->(category) { Facility.joins(:facility_categories).where('category_id = ?', category.id) }

  • 次に実際にビューやコントローラで使うメソッドです
    • メソッドの引数にcurrent_userを渡すことでそのユーザーがお気に入りに登録している動物やカテゴリーを変数に格納します
    • そしてオススメを選定するメソッドの引数にそれらを渡します
def suggest_to_login_user(user)
  animal = user.animal
  category = user.category
  recommend_data(animal, category)
end

最後にオススメ情報のロジックです ⬇︎

  • if ユーザーがお気に入りに何も登録していない場合

    • 施設のおすすめ度のカラムがsuggestの施設をすべて取得してランダムに3つ返します
  • else 動物かカテゴリのどちらか、または両方がお気に入りに設定されている場合

    • scopeの式を元に関連する施設のレコードを取得して配列にします。設定されていない場合は空配列を返します
    • 取得したレコードの2つの配列をconcatで結合してuniqで重複した値を削除した後、ランダムで3つ返します
def recommend_data(animal, category)
  facilities = Facility.all  # 施設をすべて取得して変数に格納
  if !animal && !category
    facilities.where(suggest: 1).includes(%i[facility_categories categories managements animals]).sample(3)
  else
    suggest_animal_facilities = animal ? facilities.favo_animal(animal).to_a : []
    suggest_category_facilities = category ? facilities.favo_category(category).to_a : []
    suggest_animal_facilities.concat(suggest_category_facilities).uniq.sample(3)
  end
end

このようなかんじでトップページやプロフィール画面にオススメの施設を表示しています。
直感的ではなくて読みにくく、もっといい方法があると思うので今後も改良していきたいと思います。



⬇︎ まだまだありますが長くなったので折りたたみました✨


2、プロフィール編集機能

1、プロフィール編集機能

Userモデルのform_with内で多対多の関係でアソシエーションを組んだ関連モデルの設定をする。

プロフィール編集画面でお気に入りの動物やカテゴリーを設定できる機能を実装しました。意外にもやることが多くて苦労しました。
Image from Gyazo
今回はそれぞれ一つずつしか設定できませんが、いつか複数登録できるように変更したくなった時のために多対多の関係でアソシエーションを設定しました。

1、まずユーザーと動物、カテゴリーの中間テーブルを作成してモデルに多対多のアソシエーションを設定します。

2、途中まではRailsガイドを参考にしたらそれなりに進めることができました。

3、ビューファイルのform_with内でfields_forヘルパーを使用して関連モデルのセレクトボックスを作成します。

profiles/edit.html.erb
<%= form_with model: @user, url: profile_path, local: true do |f| %>

      、、、省略、、、

  <%= f.fields_for :favorite_animal do |animal_form| %>
    <%= animal_form.select :animal_id, Animal.pluck(:name, :id) %>
  <% end %>
  <%= f.fields_for :favorite_category do |category_form| %>
    <%= category_form.select :category_id, Category.pluck(:name, :id) %>
  <% end %>
<% end %>

ここがつまずきポイントでした

Railsガイドを参考に上記のコードを書いたのですがこれではフォームが表示されません。
f.fields_forの行を以下のように修正することで解決できました。

<%= f.fields_for :favorite_animal, (current_user.favorite_animal || FavoriteAnimal.new) do |animal_form| %>
<%= f.fields_for :favorite_category, (current_user.favorite_category || FavoriteCategory.new) do |category_form| %>
  • current_user.favorite_〇〇で現在のユーザーのお気に入りのオブジェクトを設定
  • これだけだとまだ設定していない場合にセレクトボックスが表示されないのでモデル名.newでインスタンスを作成



3、Googleマップの吹き出しに多対多の関係のモデルを一覧で表示する
マップに表示されるマーカーをクリックすると吹き出しが出るのですが、施設名と住所だけだと何の施設なのか分かりにくいので関連モデルのカテゴリーを繰り返し処理で表示しました

Image from Gyazo

苦労した点

  • ActiveRecordのアソシエーションで設定したメソッドがそのままではJavaScriptでは使えない

今回コントローラで取得した施設のデータを変数に格納し、gonというgemを使ってマップの処理を記述しているJavaScriptのファイルに渡すというやり方をしました。

  def show
    @facility = Facility.find(params[:id])
    gon.facilities = [@facility]
    # これでjsファイルで facilities という変数が使える
    # 一覧と同じマップを使うので配列にする
  end

しかしこのままではJavaScriptファイルに関連モデルを渡すことができずエラーが起きました。

解決方法
  • コントローラ側で関連モデルの情報をハッシュ形式のjsonに変換する必要があります

  • 以下のようにすることでJavaScriptファイルに関連モデルを渡すことができ、アソシエーションで定義したcategoriesというメソッドが使えるようになります ⬇︎ 関連記事

  def show
    @facility = Facility.find(params[:id])
    gon.facilities = [@facility].as_json(include: :categories)
  end

次にマーカーを作成するfor文の中でカテゴリー名を表示するfor文を書く
(例)⬇︎

google_maps.js
  // カテゴリーを配列で定数cに代入
  const c = facilities[i]['categories']

  // カテゴリーの名前だけを取り出して変数に代入
  var categoriesHtml = '<ul>'
  for (let j = 0; j < c.length; j++) {
    categoriesHtml += `<li>${c[j].name}</li>`
  };
  categoriesHtml += '</ul>'

最後にカテゴリーの名前を格納した変数を吹き出しの処理に加えて完成です!

検証ツールで@facilitiesの中身を見てみると、しっかりカテゴリーがあることが分かります ⬇︎
Image from Gyazo



4、GooglePlacesAPIを使って施設情報を取得
本サービスでは本番データを作成するために GooglePlacesAPIを使用しました

処理の流れ ⬇︎

  • 1回目のリクエストでplace_idを取得するためにpredectionsを取り出して返す
  def get_data(word) # 引数には施設名を渡します
    query = URI.encode_www_form(
      input: word,
      language: 'ja',
      key: ENV['API_KEY']
    )

    url = URI("https://maps.googleapis.com/maps/api/place/autocomplete/json?#{query}")

    https = Net::HTTP.new(url.host, url.port)
    https.use_ssl = true

    request = Net::HTTP::Get.new(url)

    response = https.request(request)
    search_result = JSON.parse(response.read_body)
    search_result['predictions']
  end

  • 次に上記のメソッドを使ってplace_idを取り出し、2回目のリクエストを行う(詳細情報を取得するため)

    • 取得したJSON形式のデータをJSON.parseでrubyのハッシュ形式に変換する

    • 必要な値を取り出し、空配列dataに格納していく

  def detailed_search(prefecture, a)
    data = []

    # 検索結果の place_id を取り出しそれを元に詳細情報を検索する
    place = get_data(a).first['place_id']
    place_id = place

    place_id_query = URI.encode_www_form(
      place_id: place_id,
      language: 'ja',
      key: ENV['API_KEY']
    )
    id_search_url = URI("https://maps.googleapis.com/maps/api/place/details/json?#{place_id_query}")
    id_search_https = Net::HTTP.new(id_search_url.host, id_search_url.port)
    id_search_https.use_ssl = true
    id_search_request = Net::HTTP::Get.new(id_search_url)

    new_response = id_search_https.request(id_search_request)

    # JSON形式のデータをrubyのハッシュ形式に変換する
    new_data = JSON.parse(new_response.read_body)

    name = new_data['result']['name']
    address = new_data['result']['formatted_address']
    longitude = new_data['result']['geometry']['location']['lng']
    latitude = new_data['result']['geometry']['location']['lat']

    # 省略

    prefecture_id = prefecture

    # 空配列にデータを格納する
    data << [name, address, longitude, latitude, tel_number, opening_hours, web_site, prefecture_id]
    data
  end

  • 配列に格納したデータをCSVファイルに出力する

後は別途ファイルを作成してCSVファイルに出力するメソッドを作成、処理を実行して完成です!

  def csv_generate
    CSV.open(Rails.root.join('lib/csv/tokyo.csv'), 'a', force_quotes: true) do |csv|
      detailed_search('都道府県のid', '施設名').each do |d|
        csv << d
      end
    end
  end


5、Google広告を出して検証したり、SEO対策などをして将来的なマーケティングを意識
実際に使ってもらうためにはUI/UXやSEO対策が大事だと考え、サービスの将来的な成長を常に意識していました

行なった取り組み

1、SEO対策

  • ページのタイトルやh1タグを適切に設定する ⬇︎

Keyword Toolキーワードプランナーなどのツールを使い、キーワードの候補を決めたりキーワードの月間検索ボリュームを調査しました。

例えば 「東京 動物 ふれあい」「神奈川 動物 ふれあい」 などは月間1000回以上検索されているキーワードで、競合もあまり多くないです。
このようなかんじで ターゲットの検索キーワード を決め、タイトルやh1タグ、キーワードなどを動的に設定しました。

例) 施設詳細画面のh1タグ

<h1>
  <span><%= @facility.name %></span><%= "#{@facility.animals.pluck(:name).slice(0,3).join('')}とふれあおう営業時間などの詳細情報も紹介しています" %>
</h1>

⬇︎ こんなかんじで動的に出力されます
Image from Gyazo

  • h2 h3タグをh1タグに基づいた文章でしっかり設定

2、Google広告に出稿

1日の利用金額の上限を決めてGoogle広告を出しました

Image from Gyazo

目的1

総PV数やUU数はGoogleアナリティクスで管理していますが、実際にGoogle検索でヒットした場合はどれくらいの方にクリックしていただけるのかを検証したい
⬇︎

  • ターゲットが適切か
  • タイトルや説明が適切か & 分かりやすいか

結果

広告を出してから4日目で 表示回数 60回クリック数 9回 という結果でした。
表示回数のうち、自分が5回ほど表示したことを考慮すると5、6回表示されたら1回はクリックされるということになります。

Image from Gyazo
広告と言ってもまだ検索結果のトップではなく、2ページ目以降に表示されることを考えるとそこまで悪くない数字だと思いました。

やはりターゲットのキーワードの選定は悪くなかったと思います。

目的2

実際にGoogle検索から訪れてくれたユーザーの滞在時間や、実際にどの機能を使ってもらえるのか検証したい
⬇︎

  • 実際にアプリ内で検索をしてもらえているとしたらサービスとしてはなりたっている
  • 滞在時間を伸ばすことが目標
    • ユーザーを飽きさせない仕掛けや魅力的な何かがあれば平均の滞在時間は伸びていくと思うため

結果

広告から訪れてくれたユーザーさんの動きを検証してみると、ほとんどの方がアプリ内で実際に検索したり、トップページのリンクをクリックしていただいていることが確認できた。
  • このことからずっと意識してきた直感的な使いやすさや最低限のUIはクリアしているかなと思いました
ページごとや全体の滞在時間を見てみるとかなり短いなという印象。
  • ユーザーのニーズに合っていてもUIが魅力的じゃなかったり、夢中になる仕掛けが無かったりするとやはりすぐ飽きてしまうと思うので今後の大きな課題です

など

実装した機能一覧

ログインユーザー

  • ユーザー登録、ログイン

  • パスワードリセット機能

  • プロフィール編集機能

  • プロフィール画面に投稿した画像を一覧表示

    • 投稿した画像の削除(非同期通信)
  • お気に入りの動物、カテゴリを設定

  • お気に入りを元にオススメの施設を表示する機能

  • 写真投稿機能(詳細ページに一覧で表示される)

  • 写真が投稿されると施設のメイン画像をデフォルト画像から投稿された画像に変える

  • 施設のお気に入り機能(非同期通信)

  • 施設の報告機能

    • 報告が完了すると管理者にメールが送信されるように設定

全ユーザー

  • ユーザー登録、ログイン、ログアウト機能

  • Twitterログイン機能

  • 施設の検索

    • セレクトボックス
    • フリーワード検索
  • 施設情報の閲覧

  • マップを表示

    • 現在地を取得して表示
  • Twitter、LINEで施設を共有する機能

など

使用技術

バックエンド

  • Ruby 3.1.2
  • Rails 6.1.6

主要ライブラリ

  • Sorcery(ログイン)
  • SeedFu(データ作成)
  • CarrierWave(画像アップロード)
  • ActiveHash(擬似的なテーブル作成)
  • MetaTags(SEO)
  • RSpec(テスト)
  • RuboCop(リントチェック)

フロントエンド

  • HTML/SCSS/JavaScript
  • CSSフレームワーク
    • Tailwind CSS
    • daisyUI

インフラ

  • Heroku
  • PostgreSQL(データベース)
  • Amazon S3(本番環境の画像アップロード)
  • SendGrid(本番環境のメール送信)

その他

  • TwitterAPI(Twitterログイン)
  • GoogleMapsAPI
  • GooglePlacesAPI(施設データ取得)

ER図

Image from Gyazo

おわりに

最後までご覧いただきありがとうございました!

実際に自分でサービスを作成する過程は本当に楽しくて夢中になってしまいました。
デザインやSEO対策、冗長なコードなど課題は多いですがこれからもサービスを改善していったり、新しい技術にチャレンジしていきたいと思います!

▼ FLUFF MAPS
https://www.fluff-maps.com

▼ Twitterアカウント
https://twitter.com/kazuki_okumura

追記(2024年3月31日)

おかげさまでUU数34,000、PV数88,000を突破しました!
SEO対策の効果が日に日に出てきて、多い日ではGoogle検索から200以上クリックされる日もあります。

クラウドの利用料金の影響などもあり、一時期サービスの提供をストップしていた時期もありましたがもっとたくさんの方々に使っていただけるようにサービスの発展に力を入れていこうと思います。
これからもFLUFF MAPSをよろしくお願いいたします!🐥✨

34
25
0

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
34
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?