PHP
Redis
laravel
predis
laravel5

Laravel + redis で閲覧回数ランキングを作る

More than 1 year has passed since last update.

はじめに

いろんなサイトで見かける「よく見られている記事」的な機能を
Laravelプロジェクトにredisで実装しました。
一部サンプル化して共有します。

サンプルコードは以下
https://github.com/esuEichi/sample_view_ranking_with_laravel_redis.git

手っ取り早く動きだけ見たいときは、上記のサンプルコードをcloneした後、
composer install
composer update
を実行し最下部に記載の結果にある手順で動作確認ができるかと思います。

今回の学び

  • redis はキーバリューなので、基本的に一意の値で表現できるのは一つ
    • categoryと投稿日でランキングを作るなどのときはkeyの値を工夫する必要がある

困ったこと

  • 今回作成したRankingModluleをControllerとして作っていますが、MVCとしてのControllerの役割は持っていないため、なんとなく気持ち悪い

考え方

  • 記事IDをもとに一意なredisのキーを作成する
  • 表示のためのController が呼ばれたときredisにキーがあるかを確認する
    • なければvalue1にセット
    • あればvalueをインクリメント
  • キーから記事IDとビュー数を持った配列を作る
  • value順にソートしてviewへ渡す
  • viewでforeachで出力する

手順

プロジェクトを作る

composer create-project laravel/laravel sample_ranking_with_redis

composer で redis を入れる

以下のようにcomposer.jsonへpredisを追記してください。

composer.json
    "require": {
        "php": ">=5.6.4",
        "laravel/framework": "5.4.*",
        "laravel/tinker": "~1.0",
        "predis/predis":"^1.1"
    },

追記したら以下コマンドでredisを使える状態にします。

composer install
composer update

redisの立ち上げ

以下コマンドでredisを立ち上げます。簡単ですね。
redis-server

route/webの書き換え

デフォルトのtopへのパスはコメントアウトして、以下二行を足します。
基本的には、topへの遷移時にControllerを経由するようにするのと、
記事ページ(LinkView)への遷移時にControllerを経由するようにします。この時、
記事のIDをControllerで受け取るようにしておきます。

web.php追加分
 Route::get('/', 'TopController@index');
 Route::get('/{id}','LinkController@index');

Ranking機能に必要なファイルの作成

RankingModule の作成

今回の肝となるランキング部分を司るモジュールを作成します。
まずは以下のコマンドでControllerとして作成します。
php artisan make:contoller RankingModule

以下の2つのメソッドを用意します。

increment_view_ranking メソッド

ユーザーによって記事ページを参照されたときに呼び出され、
閲覧数をインクリメントするメソッドです。
IDを引数として受け取るようにしておくことで
呼び出し元はlinkControllerになります。

get_ranking_all メソッド

ランキングの結果を配列で返すメソッドです。
redisからキーをすべて取得し、valueの値でソートした配列を返します。
呼び出し元はTopControllerになります。
中身は以下

RankingModule.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Redis;


class RankingModule extends Controller
{
    //
    public function increment_view_ranking($id){
        $key = "ranking-"."id:".$id;

        $value = Redis::get($key);
        if(empty($value)){
            Redis::set($key, "1");
            Redis::expire($key, 60*60); 
        } else {
            Redis::set($key,$value + 1);
        }
        echo "view count:".Redis::get($key);
    }
    public function get_ranking_all(){
        $keys = Redis::keys('ranking-*');
        $results = Array();

        if(empty($keys) != true){
            for($i=0; $i < sizeof($keys); $i++){
                $point = Redis::get($keys[$i]);
                $id = explode(':', $keys[$i])[1];
                $results[$id] = $point;
            }
            arsort($results, SORT_NUMERIC);
        }
        return $results;
    }
}

link view の作成

今回はデフォルトのwelcomeテンプレートをコピペしてリネームしただけとします。

LinkController の作成

上で作ったlinkへの遷移を行うControllerです。

LinkController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class LinkController extends Controller
{
    //
    function index(Request $request){
        $id = $request->id;
        $ranking = new RankingModule;
        $ranking->increment_view_ranking($id);
        return view('link')->with(compact('id'));
    }
}

welocome viewに記事へのリンク先を作成し、ランキングを作成する

TopControllerの作成

route/web.phpで書き換えたように、Topページ(welcomeView)への遷移を司るコントローラーです。
役割としてはRankingModuleから現在のランキング表示用の配列を作成し、
viewへ渡すことです。

TopController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
class TopController extends Controller
{
    //
    function index(Request $request){
        $ranking = new RankingModule;
        $results = $ranking->get_ranking_all();
        return view('welcome')->with(compact('results'));
    }
}

welcomeviewの書き換え

welcomeview(topページ)にはリンク先を作成します。

記事へのリンク

topページから遷移できるダミーの記事ページへのリンクを作成します。
今回はダミーなのでfor文で10記事リンクを作成し、各記事へのIDとします。

ランキングの作成

TopControllerから受け取ったランキング用の結果配列を使い、
閲覧が多い順に該当の記事へのリンクを作成します。ついでに、何回見られているのかもリンクに表示しておきます。
ついでにランキング用の配列もdumpで吐き出しておきます。

welcomeView追加分
3d2
< 
85,94c84,90
<                 <div class="test_links">
<                 @for ($i = 0; $i < 10; $i++)
<                     <div><a href="./{{{$i}}}">link_{{{$i}}}</a></div>
<                 @endfor
<                 </div> 
<                 <div class="ranking">
<                 @foreach($results as $id => $point)
<                     <div><a href="./{{{$id}}}">link_{{{$id}}}_view_count_is_{{{$point}}}</a></div>
<                 @endforeach
<                 <?php var_dump($results);?>

結果

以下2つのコマンドを打つ
php artisan serve
redis-server

http://localhost:8000
へアクセス
http://localhost:8000/1
などのページを見ると、一番下の順番が変わっていきます。

スクリーンショット 2017-04-16 15.22.36.png

拡張性について

特定のカテゴリなど条件で絞り込んだランキングが作りたいとき

redisのキーは今回以下の様になってます。
$key = "ranking-"."id:".$id;
例えばカテゴリが複数あって、/category ページで配下コンテンツのランキングを作りたいときはまずキーを以下の様にします。

$key = "ranking-"."id:".$id."_category:".$category;

こうすることで全体ランキングだけではなく、categoryでkeyを取得することでカテゴリランキングを作成することができます。

終わりに

実際のサービスではもう少し拡張して、拡張性に書いたようなカテゴリごとのランキングと、全体でのランキングとして使っています。
ランキングを作ることで弱小サービスでもユーザーが使ってくれてる感じが出てきて
楽しくなってきます。

補足

niisan-tokyo さんからredis の sorted set 機能を使うのが楽だとご指摘をいただきました。
記事は以下です。
http://qiita.com/niisan-tokyo/items/170ef3ac47ee39cfb46c

確かに今回のように無期限かつ常に同一のキーの値を参照し続ける場合はこちらのほうが良さそうだし、
計算コストも少なそうです。

ご指摘ありがとうございました。