From 41b30fc64cf7e427b057b2766b6ad771b6d4fb3b Mon Sep 17 00:00:00 2001 From: evazion Date: Sun, 1 Dec 2019 00:34:30 -0600 Subject: [PATCH] recommendations: open user recommendations to all users. * Open recommendations to all users (not just gold). * Show recommendations on all posts (not just posts after 2017). * Allow users to browse recommendations for other users. * Increase number of recommended posts returned. * Change endpoints to /recommended_posts?user_id=1234 and /recommended_posts?post_id=1234 and add json/xml support. --- .../recommended_posts_controller.rb | 25 +++----- app/javascript/src/javascripts/posts.js.erb | 5 +- app/logical/recommender_service.rb | 40 ++++++++++++ app/models/recommender_service.rb | 62 ------------------- .../partials/common/_secondary_links.html.erb | 4 +- app/views/recommended_posts/show.html.erb | 2 +- app/views/recommended_posts/show.js.erb | 1 + 7 files changed, 56 insertions(+), 83 deletions(-) create mode 100644 app/logical/recommender_service.rb delete mode 100644 app/models/recommender_service.rb create mode 100644 app/views/recommended_posts/show.js.erb diff --git a/app/controllers/recommended_posts_controller.rb b/app/controllers/recommended_posts_controller.rb index 5bcb41f12..9ee25479a 100644 --- a/app/controllers/recommended_posts_controller.rb +++ b/app/controllers/recommended_posts_controller.rb @@ -1,23 +1,18 @@ class RecommendedPostsController < ApplicationController - before_action :member_only - respond_to :html + respond_to :html, :json, :xml, :js def show - @posts = load_posts() + @max_recommendations = params.fetch(:max_recommendations, 100).to_i.clamp(0, 1000) - if request.xhr? - render partial: "show", layout: false + if params[:user_id].present? + @recs = RecommenderService.recommend_for_user(params[:user_id], @max_recommendations) + elsif params[:post_id].present? + @recs = RecommenderService.recommend_for_post(params[:post_id], @max_recommendations) + else + @recs = [] end - end -private - - def load_posts - if params[:context] == "post" - @posts = RecommenderService.recommend(post_id: params[:post_id].to_i) - - elsif params[:context] == "user" - @posts = RecommenderService.recommend(user_id: CurrentUser.id) - end + @posts = @recs.map { |rec| rec[:post] } + respond_with(@recs) end end diff --git a/app/javascript/src/javascripts/posts.js.erb b/app/javascript/src/javascripts/posts.js.erb index d4974c4bd..26ce1aae2 100644 --- a/app/javascript/src/javascripts/posts.js.erb +++ b/app/javascript/src/javascripts/posts.js.erb @@ -9,6 +9,7 @@ let Post = {}; Post.pending_update_count = 0; Post.SWIPE_THRESHOLD = 60; Post.SWIPE_VELOCITY = 0.6; +Post.MAX_RECOMMENDATIONS = 27; // 3 rows of 9 posts at 1920x1080. Post.initialize_all = function() { @@ -433,9 +434,7 @@ Post.initialize_post_sections = function() { $("#comments").hide(); $("#edit").hide(); $("#recommended").show(); - $.get("/recommended_posts", {context: "post", post_id: Utility.meta("post-id")}, function(data) { - $("#recommended").html(data); - }); + $.get("/recommended_posts.js", { post_id: Utility.meta("post-id"), max_recommendations: Post.MAX_RECOMMENDATIONS }); } else { $("#edit").hide(); $("#comments").hide(); diff --git a/app/logical/recommender_service.rb b/app/logical/recommender_service.rb new file mode 100644 index 000000000..8d1764775 --- /dev/null +++ b/app/logical/recommender_service.rb @@ -0,0 +1,40 @@ +module RecommenderService + module_function + + MIN_POST_FAVS = 5 + MIN_USER_FAVS = 50 + CACHE_LIFETIME = 4.hours + + def enabled? + Danbooru.config.recommender_server.present? + end + + def available_for_post?(post) + enabled? && post.fav_count > MIN_POST_FAVS + end + + def available_for_user?(user) + enabled? && user.favorite_count > MIN_USER_FAVS + end + + def recommend_for_user(user_id, limit = 50) + body, status = HttpartyCache.get("#{Danbooru.config.recommender_server}/recommend/#{user_id}", params: { limit: limit }, expiry: CACHE_LIFETIME) + return [] if status != 200 + + process_recs(body) + end + + def recommend_for_post(post_id, limit = 50) + body, status = HttpartyCache.get("#{Danbooru.config.recommender_server}/similar/#{post_id}", params: { limit: limit }, expiry: CACHE_LIFETIME) + return [] if status != 200 + + process_recs(body).reject { |rec| rec[:post].id == post_id } + end + + def process_recs(recs) + recs = JSON.parse(recs).to_h + recs = Post.where(id: recs.keys).map { |post| { score: recs[post.id], post: post } } + recs = recs.sort_by { |rec| -rec[:score] } + recs + end +end diff --git a/app/models/recommender_service.rb b/app/models/recommender_service.rb deleted file mode 100644 index 777c300fc..000000000 --- a/app/models/recommender_service.rb +++ /dev/null @@ -1,62 +0,0 @@ -module RecommenderService - extend self - - SCORE_THRESHOLD = 5 - - def enabled? - Danbooru.config.recommender_server.present? - end - - def available_for_post?(post) - return true if Rails.env.development? - - enabled? && post.created_at > Date.civil(2017, 1, 1) && post.fav_count >= SCORE_THRESHOLD - end - - def available_for_user? - enabled? && CurrentUser.is_gold? - end - - def recommend_for_user(user_id) - ids = Cache.get("rsu:#{user_id}", 1.hour) do - resp = HTTParty.get( - "#{Danbooru.config.recommender_server}/recommend/#{user_id}", - Danbooru.config.httparty_options.merge( - basic_auth: { - username: "danbooru", - password: Danbooru.config.recommender_key - } - ) - ) - JSON.parse(resp.body) - end - Post.find(ids.map(&:first)) - end - - def recommend_for_post(post_id) - ids = Cache.get("rss:#{post_id}", 1.hour) do - resp = HTTParty.get( - "#{Danbooru.config.recommender_server}/similar/#{post_id}", - Danbooru.config.httparty_options.merge( - basic_auth: { - username: "danbooru", - password: Danbooru.config.recommender_key - } - ) - ) - JSON.parse(resp.body) - end - if ids.is_a?(Hash) # error state - return [] - end - Post.find(ids.reject {|x| x[0] == post_id}.map(&:first)) - end - - def recommend(post_id: nil, user_id: nil) - if post_id - recommend_for_post(post_id) - elsif user_id - recommend_for_user(user_id) - end - end -end diff --git a/app/views/posts/partials/common/_secondary_links.html.erb b/app/views/posts/partials/common/_secondary_links.html.erb index 9c9eda2de..a95ce54c8 100644 --- a/app/views/posts/partials/common/_secondary_links.html.erb +++ b/app/views/posts/partials/common/_secondary_links.html.erb @@ -2,8 +2,8 @@ <%= subnav_link_to "Listing", posts_path %> <%= subnav_link_to "Upload", new_upload_path %> <%= subnav_link_to "Hot", posts_path(:tags => "order:rank", :d => "1") %> - <% if RecommenderService.available_for_user? %> - <%= subnav_link_to "Recommended", recommended_posts_path(context: "user") %> + <% if RecommenderService.available_for_user?(CurrentUser.user) %> + <%= subnav_link_to "Recommended", recommended_posts_path(user_id: CurrentUser.id) %> <% end %> <% unless CurrentUser.is_anonymous? %> <%= subnav_link_to "Favorites", posts_path(tags: "ordfav:#{CurrentUser.user.name}") %> diff --git a/app/views/recommended_posts/show.html.erb b/app/views/recommended_posts/show.html.erb index 07ce6239c..5aa5e410f 100644 --- a/app/views/recommended_posts/show.html.erb +++ b/app/views/recommended_posts/show.html.erb @@ -2,7 +2,7 @@

Recommended Posts

-

Based on your favorites, you may enjoy these posts. Favorite more to get more accurate results. These recommendations update every hour.

+

Based on your favorites, you may enjoy these posts. Favorite more posts to get more accurate results. These recommendations update every few hours.

<%= render "posts/partials/common/inline_blacklist" %> <%= render partial: "show" %> diff --git a/app/views/recommended_posts/show.js.erb b/app/views/recommended_posts/show.js.erb new file mode 100644 index 000000000..e7522adc7 --- /dev/null +++ b/app/views/recommended_posts/show.js.erb @@ -0,0 +1 @@ +$("#recommended").html("<%= j render "recommended_posts/show" %>");