Nwht0xn1

amakanのランキングの実装Created on 2016-07-09 by r7kamura

ランキングの仕組みを改善しました - amakan で入れた実装の話。

View

@products の中にランキング順に並べられた本が入っているが、これを計算するのに少し時間がかかるのでキャッシュしてある。@products.each が実行されたタイミングで計算処理が走るので、このタイミングでキャッシュしておけば大丈夫。

- cache "daily_products", expires_in: 4.hours do
  - @products.each.with_index do |product, index|
    = render "product", index: index, product: product

Controller

Amakan::ProductRankingCalculation のインスタンスを @products に入れています。このインスタンスは、.each が呼ばれたときに初めてSQLを発行するようになっている。

class ProductsController < ApplicationController
  def index
    @products = ::Amakan::ProductRankingCalculation.new(duration: 1.day, limit: 60)
  end
end

ライブラリ側

ランキング計算自体のコード。「読んだ」「読みたい」ボタンが押されるたびに evaluations というテーブルにレコードが保存されるようになっているので、このレコードを利用して、最近最もevaluationsのレコードが多く作られた本 (products) を計算する。

module Amakan
  class BaseRankingCalculation
    include ::Enumerable

    delegate(:each, to: :call)

    # @param duration [ActiveSupport::Duration]
    # @param limit [Integer]
    # @return [ActiveRecord::Relation]
    def initialize(duration:, limit:)
      @duration = duration
      @limit = limit
    end

    # @return [ActiveRecord::Relation]
    def call
      raise ::NotImplementedError
    end
  end

  class ProductRankingCalculation < BaseRankingCalculation
    # @note Override
    def call
      order = ::ActiveRecord::Base.send(:sanitize_sql_array, ["field(id,?)", product_ids])
      ::Product.where(id: product_ids).order(order)
    end

    private

    # @return [Array<Integer>]
    def product_ids
      ::Evaluation.select(:product_id).where("created_at > ?", @duration.ago).each_with_object(Hash.new(0)) do |evaluation, hash|
        hash[evaluation.product_id] += 1
      end.sort_by do |product_id, count|
        -count
      end.map do |product_id, count|
        product_id
      end.take(@limit)
    end
    memoize :product_ids
  end
end