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