hummingbird-me/kitsu-server

View on GitHub
app/services/trending_service.rb

Summary

Maintainability
A
45 mins
Test Coverage
F
54%
class TrendingService
  EPOCH = Date.new(2030, 1, 1).to_time.to_i
  ITEM_LIMIT = 200
  NETWORK_LIMIT = 20
  CATEGORY_LIMIT = 20
  TRIM_PROBABILITY = 0.1

  attr_reader :namespace, :half_life, :user, :token

  def initialize(namespace, half_life: 7.days.to_i, user: nil, token: nil)
    @namespace = namespace
    @half_life = half_life
    @token = token
    @user = user || token&.resource_owner
  end

  def vote(id, weight = 1.0)
    key = trending_key
    update_score(key, id, weight)
    trim(key, limit: ITEM_LIMIT) if rand < TRIM_PROBABILITY
    if user
      TrendingFanoutWorker.perform_async(namespace.name, half_life, user&.id, id,
        weight)
    end
  end

  def fanout_vote(id, weight = 1.0)
    followers.each do |uid|
      key = trending_key(uid)
      update_score(key, id, weight)
      trim(key, limit: NETWORK_LIMIT) if rand < TRIM_PROBABILITY
    end

    handle_trending_categories(id, weight)
  end

  def get(limit = 5)
    results_for(trending_key, limit)
  end

  def get_network(limit = 5)
    key = trending_key(user_id)
    results_for(key, limit)
  end

  def get_category(id, limit = 5)
    key = trending_category_key(id)
    results_for(key, limit)
  end

  def trim(key, limit: 100)
    $redis.with do |conn|
      conn.zremrangebyrank(key, 0, -limit)
    end
  end

  private

  def results_for(key, limit = 5, offset = 0)
    results = []
    loop.with_index do |_, index|
      break if index > 5
      page = raw_results_for(key, limit, offset)
      results += enrich(page) if enrichable?
      break if results.count > limit
      offset += limit
    end
    results[0...limit]
  end

  def raw_results_for(key, limit = 5, offset = 0)
    stop = offset + limit - 1
    $redis.with do |conn|
      conn.zrevrange(key, offset, stop)
    end
  end

  def trending_key(uid = nil)
    ns = namespace
    ns = ns.table_name if enrichable?
    key = "trending:#{ns}"
    key += ":user:#{uid}" if uid
    key
  end

  def trending_category_key(category_id)
    ns = namespace
    ns = ns.table_name if enrichable?
    key = "trending:#{ns}"
    key += ":category:#{category_id}"
    key
  end

  def handle_trending_categories(id, weight = 1.0)
    return unless %w[Anime Manga Drama].include?(namespace.name)
    categories = namespace.includes(:categories).find_by(id: id).categories
    categories.each do |category|
      key = trending_category_key(category.id)
      update_score(key, id, weight)
      trim(key, limit: CATEGORY_LIMIT) if rand < TRIM_PROBABILITY
    end
  end

  def update_score(key, id, weight = 1.0)
    $redis.with do |conn|
      conn.zincrby(key, change_for(weight), id)
    end
  end

  def change_for(weight)
    weight * (2.0**((Time.now.to_i - EPOCH).to_f / half_life))
  end

  def followers
    Follow.where(followed_id: user_id).pluck(:follower_id)
  end

  def enrich(ids)
    ids = ids.map(&:to_i)
    scope = Pundit.policy_scope!(token, namespace)
    instances = scope.where(id: ids).index_by(&:id)
    ids.collect { |id| instances[id] }.compact
  end

  def enrichable?
    namespace.respond_to? :table_name
  end

  def user_id
    user.respond_to?(:id) ? user.id : user
  end
end