読者です 読者をやめる 読者になる 読者になる

ほろ酔い開発日誌

有意義な技術的Tipsを共有出来たら嬉しいです。Ruby、Railsが好きです。Web開発全般(Rails多め、フロント、サーバー、インフラ)、データ分析、機械学習あたりの記事が中心になる予定です。

Rails RedisでPVランキングを作ってみた

記事のPV数ランキングを実装したかったので、Redisを利用して実装してみたいと思います。 ちなみに、Redisを使うに至った経緯は、

  • MySQLにPVテーブルを持たせると負荷上がりそうだから避けたい。
  • とはいっても簡単にランキング機能を実装したい。
  • じゃあ、Google AnarithicsのAPIかRedisが良さそう。
  • Redisで作ったことないしちょっと触ってみるか!

といった感じです。

オススメの実装方法等ランキング機能実装の知見がある方はコメントにてご教示頂けますと幸いです。

というわけで、実装します。今回はlocal環境までしかやっていません。AWSのElasticCacheでproductionは用意しようと思っています。

準備

Rails + Redis + AWSでPV数を保存

こちらの記事が今回方針を立てる上で役立ちました。

$ brew install redis
$ redis-server # redisの起動

でredisを用意。

コマンドに関してはこちら記事を参考に。

Redisサーバー、Redisクライアントの起動と終了

次に初期設定を書きます。環境変数の設定をして下さい。 僕は.envに書きましたが、ご自由に環境変数を設定して下さい。

REDIS=localhost:6379

gemファイルに

gem 'redis'

でインストールして

以下のconfig/initializer/redis.rb を追加します。

require 'redis'

uri = URI.parse(ENV["REDIS"])
REDIS = Redis.new(host: uri.host, port: uri.port)

これで準備は完了です。

PVを登録する

ランキングの仕組みは以下の記事を参考にしました。

Redisでアクセスランキングを実装

記事の個別ページにアクセスしたときにそのアクセスデータを保存します。

  def show(id)
    @article = Article.find(id)
    REDIS.zincrby "articles/daily/#{Date.today.to_s}", 1, @article.id
  end

Method: Redis#zincrby

zincrbyというメソッドはキーに対して指定されたメンバーの値のインクリメントが出来ます。

ここで「?」となるかもなので、Redisとは何ぞやって話なのですけど、KVS(キーバリューストア)というDBの仲間です。キーに対してバリューを持つという単純な構造を持ったDBです。

rubyっぽく表すとkey: valueみたいなのがKVSの基本構造で、今回使うのはソート済みセット型と呼ばれるkey: { member: score }のような構造だと考えると分かりやすいかと思います。以下のように値が入ります。

key member score
2016-5-2 123 2
2016-5-2 124 6
2016-5-2 125 1
2016-5-2 126 4

Redisに関してはこちらも参考になりました。

Redisの使い方

話を戻して今回は以下のような書き方をしました。

REDIS.zincrby "articles/daily/#{Date.today.to_s}", 1, @article.id

これは key "articles/daily/#{Date.today.to_s}"に対してvalueは member @article.idの scoreに 1プラスするという意味を表します。

つまり、アクセスするごとに1足してPVをカウントするのです。ちなみに該当するキー・メンバーがいないときは勝手に作ってくれるので便利です。

ランキングを表示する

以下のような感じにPVを取り出してみました。昨日のPVランキングです。

  def set_ranking_data
    ids = REDIS.zrevrangebyscore "articles/daily/#{Date.yesterday.to_s}", "+inf", 0, limit: [0, 5]
    @ranking_articles = ids.map{ |id| Article.find(id) }
    if @ranking_articles.count < 5
      adding_articles = Article.order(published_at: :DESC, updated_at: :DESC).where.not(id: ids).limit(5 - @ranking_articles.count)
      @ranking_articles.concat(adding_articles)
    end
  end
ids = REDIS.zrevrangebyscore "articles/daily/#{Date.yesterday.to_s}", "+inf", 0, limit: [0, 5]

こちらのメソッドなのですが以下のURLを参照して下さい。

Method: Redis#zrevrangebyscore

説明しますと、zrevrangebyscore が指定したキーのバリューをscore順に並び替えてmemberをscoreの高い順に返してくれる便利なメソッドです。"+inf"は値の上限はなしを意味し、0は値の下限が0であることを示します。値の制限をつけるのはパフォーマンスで必要になることがあるかららしいのですが、今回の場合はこのような設定で良いと思いました。それでlimitは何件取得するかです。今回は上位5件を取り出します。

ここでArticleのインスタンスを取り出しています。

@ranking_articles = ids.map{ |id| Article.find(id) }

間違っても以下のようにしてはだめです。これだとランキング順ではなくid順で取得されます。

@ranking_articles = Article.where(id: ids)

あとは、idsが5件未満の対策をしておきました。必ず5件取得したかったので。

    if @ranking_articles.count < 5
      adding_articles = Article.order(published_at: :DESC, updated_at: :DESC).where.not(id: ids).limit(5 - @ranking_articles.count)
      @ranking_articles.concat(adding_articles)
    end
  end

以上でRedisによるランキング機能の説明は終わります。

週ごとのランキングや月ごとのランキングも出来そうなので必要になったらやってみようと思います。