現在ポートフォリオとして大学受験をテーマにしたQ&Aサイトを作成しています
関連する質問を表示する機能の実装に手間取ったので、自分用にまとめ。
何をもって関連とするか
質問同士を関連づけるための基準が必要ですが、今回は質問にカテゴリーを紐付け、同じカテゴリーに属するものを関連する質問とします
モデル
has_many :category_relationships
has_many :categories, through: :category_relationships, source: :category
belongs_to :question
belongs_to :category
has_many :category_relationships
has_many :questions, through: :category_relationships, source: :question
実装
実現したい動き
questionモデルのインスタンスメソッドとして、関連する質問を取得するrelated_questionsメソッドを追加します
- 特定の質問から、カテゴリーを取得
- そのカテゴリー毎の質問を取得
- 上記の操作で得た質問の配列を「関連する質問」とし、そのいくつかをランダムで表示する(今回は4つ)
- 質問の重複と、レシーバーとなる質問自身を含まないように配慮
このような形で実装していきたいと思います
1.特定の質問から、カテゴリーを取得
- CategoryRelationshipテーブルから、question_idがレシーバーと等しいレコードを取得
- そのレコードからカテゴリーを取得
- 上記の結果をrelated_categoriesという変数にいれる
可読性を考慮して、selfをつけておきます
def related_questions
related_categories = CategoryRelationship.where(question_id: self.id).map(&:category)
end
2.カテゴリー毎の質問を取得
- 質問を入れておくために、related_questionsというからの配列を定義
- related_categories内のカテゴリー一つ一つから、関連する質問を取得する
- その質問をrelated_questionsに入れていく
def related_questions
related_categories = CategoryRelationship.where(question_id: self.id).map(&:category)
# ここから
related_questions = []
related_categories.each do |category|
category.questions.each do |question|
related_questions << question
end
end
# ここまで追加
end
eachがネストしちゃってますが…とりあえず動くのでこのままで。
より良い方法を思いついたら追記します
3.ランダムで取得
明示的にreturnをつけておきます
def related_questions
related_categories = CategoryRelationship.where(question_id: self.id).map(&:category)
related_questions = []
related_categories.each do |category|
category.questions.each do |question|
related_questions << question
end
end
# ここから
return related_questions.sample(4)
# ここまで追加
end
4.重複を避ける
このままでは複数のタグをつけている質問が重複して取得される可能性がありますね
related_questions.distinct.sample(4)
的なことをしたいんですが、配列に対してdistinctを使うとエラーが発生します
Question.first.related_questions.distinct.sample(4)
=> NoMethodError: undefined method `distinct' for #<Array:xxxxx>
配列から重複を取り除いてくれるメソッドを探してみたところ、uniqというメソッドを発見しました
https://docs.ruby-lang.org/ja/latest/method/Array/i/uniq.html
これを使いましょう
def related_questions
related_categories = CategoryRelationship.where(question_id: self.id).map(&:category)
related_questions = []
related_categories.each do |category|
category.questions.each do |question|
related_questions << question
end
end
return related_questions.uniq.sample(4) #この行に追記
end
5.レシーバー自身を含めない
このままではrelated_questions内にレシーバー自身が含まれてます
related_questions.distinct.where.not(questions_id: self.id).sample(4)
的なことをしたいんですが、配列に対してwhereを使うと上と同じエラーが発生します
ここは素直にif文を使っていきます
def related_questions
related_categories = CategoryRelationship.where(question_id: self.id).map(&:category)
related_questions = []
related_categories.each do |category|
category.questions.each do |question|
related_questions << question unless question == self #この行に追記
end
end
return related_questions.uniq.sample(4)
end
これでメソッドは完成です!
N+1対策もやっておきたいところです
表示するまでの部分は省略します
最後に
最初に書きましたが、何をもって関連とするのかが重要な気がします
他のサイトの関連するものを表示する機能ってどうなってるんですかね…
同じカテゴリー内でPV数、いいね数を基準にする形の実装もやってみたいです
おかしい部分への指摘やアドバイスなどいただけると嬉しいです
自分のポートフォリオでは今回の実装とコードが少し異なるのですが、大体同じ流れで実装してます。
テストも書いてるので良かったらのぞいてみてください
https://github.com/YutoKashiwagi/Ukarimi/pull/93/files
こっちのコードについての意見も大歓迎です!(むしろこっちに対するレビューが欲しいです)
読んでいただきありがとうございました!