Laravelで「この商品を見た人はこちらの商品も見ています」機能をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、普段から私はインターネット上でお買い物をすることが多いのですが、Amazonなどで見かけるあるコンテンツがいつも気になってしまいます。

その機能は・・・・・・

「この商品を見た人はこちらの商品も見ています」

機能です。

やはり同じ商品を見た他の人たちは、好みが似てるわけで、よく「おっ、分かってるね〜♪」なんていいながら思わずポチってしまいます😂

そこで❗

今回はこの「おすすめ紹介機能」をLaravelでつくる方法をご紹介します。

ぜひ皆さんのお役にたてると嬉しいです😊✨
(最後にソースコード一式をダウンロードできますよ👍)

「鼻うがいするようになって、
体調良くなりました😊」

どうやっておすすめ商品を探すか?

ある商品を見た人たち」がチェックした他の商品を抽出するには以下の手順をとります。

  1. ある商品にアクセスした人たちのIPアドレスを取得
  2. そのIPアドレスがアクセスした他の商品を取得
  3. 商品ごとにアクセス回数(関連度)を集計してデータ保存

では、実際の作業にいってみましょう❗

モデル&マイグレーションをつくる

まずは、モデル&マイグレーション(≒DBテーブル設計書)をつくりますが、全部で3セット必要になるのでひとつずつ紹介します。

  • 商品
  • 商品へのアクセス
  • おすすめ商品

商品

商品データを管理するためのものです。
以下のコマンドを実行してください。

php artisan make:model Product -m

モデルとマイグレーションのファイルが作成されますので中身を次のように変更します。

/app/Product.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // 👇 リレーションシップ
    public function accesses() {

        return $this->hasMany('App\ProductAccess', 'product_id', 'id');

    }

    public function recommendations() {

        return $this->hasMany('App\ProductRecommendation', 'original_product_id', 'id')
            ->orderBy('access_count', 'desc');

    }
}

この中のaccesses()は、後でつくるproduct_accessesテーブルとの、そしてrecommendations()product_recommendationsとのリレーションシップで、どちらも「1:多」の関係になります。

そして、マイグレーションです。

/database/migrations/****_**_**_******_create_products_table.php

<?php

// 省略

class CreateProductsTable extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name')->comment('商品名');
            $table->string('image_url')->nullable()->comment('画像URL');
            $table->dateTime('recommendation_updated_at')->nullable()->comment('おすすめ商品を更新した日時');
            $table->timestamps();
        });
    }

    // 省略

}

recommendation_updated_atは、おすすめ商品を集計した日時が保存されることになり、この内容をチェックして3日ごとにおすすめデータを自動更新したりします。(後ほどご紹介します)

商品へのアクセス

続いて「ある商品を見たことが分かるデータ」を管理するモデル&マイグレーションをつくります。

以下のコマンドを実行してください。

php artisan make:model ProductAccess -m

後で出てくるコードを実行する際に、MassAssignmentExceptionが出ないよう$guardedを追加しておきます。

/app/ProductAccess.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProductAccess extends Model
{
    protected $guarded = ['id']; // 👈 追加
}

そして、マイグレーションです。

/database/migrations/****_**_**_******_create_product_accesses_table.php

<?php

// 省略

class CreateProductAccessesTable extends Migration
{
    public function up()
    {
        Schema::create('product_accesses', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('product_id')->comment('商品ID');
            $table->string('ip')->comment('IPアドレス');
            $table->timestamps();

            $table->foreign('product_id')->references('id')->on('products');
            $table->unique(['product_id', 'ip']);
        });
    }

    // 省略

}

※なお、特定の商品でIPアドレスが重複しないように、product_idipでユニーク設定しています。

おすすめ商品

最後のモデル&マイグレーションは、集計したおすすめ商品データを管理するものです。

以下のコマンドを実行してください。

php artisan make:model ProductRecommendation -m

作成されたモデルにリレーションシップを追加します。

/app/ProductRecommendation.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProductRecommendation extends Model
{
    // リレーションシップ
    public function product() {

        return $this->belongsTo('App\Product', 'product_id', 'id');

    }
}

同じく、マイグレーションです。

/database/migrations/****_**_**_******_create_product_recommendations_table.php

<?php

// 省略

class CreateProductRecommendationsTable extends Migration
{
    public function up()
    {
        Schema::create('product_recommendations', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('product_id')->comment('おすすめ商品ID');
            $table->unsignedBigInteger('original_product_id')->comment('元の商品ID');
            $table->integer('access_count')->comment('アクセス件数');
            $table->timestamps();

            $table->foreign('product_id')->references('id')->on('products');
            $table->foreign('original_product_id')->references('id')->on('products');
        });
    }

    // 省略
}

表現が難しいのですが、product_idが「おすすめ商品のID」で、original_product_idが「この商品を見た人は・・・」の「この商品」のIDになります。(つまり、派生させる元の商品)

そして、access_countは集計したアクセス回数を保存するので、この数字が大きければ大きいほど関連が深いと言えます。

では、この時点で一度マイグレーションを実行してテーブルの中身を見てみましょう。

php artisan migrate

テーブルはこうなりました。

Seederでテストデータをつくる

次に開発がしやすいように、テストデータ(Seeder)を用意しておきましょう。
以下のコマンドを実行してください。

php artisan make:seed ProductsTableSeeder

すると、Seederファイルが作成されるので、中身を次のようにします。

/database/seeds/ProductsTableSeeder.php

<?php

// 省略

class ProductsTableSeeder extends Seeder
{
    public function run()
    {
        for($i = 1 ; $i <= 25 ; $i++) {

            $product = new \App\Product();
            $product->name = '商品名 '. $i;
            $product->image_url = 'https://via.placeholder.com/150?text=Product '. $i;
            $product->save();

        }

        $products = \App\Product::get();

        for($i = 1 ; $i <= 100 ; $i++) {

            $product_id = $products->random()->id;
            $ip = Arr::random([
                '1.1.1.1',
                '2.2.2.2',
                '3.3.3.3',
                '4.4.4.4',
                '5.5.5.5',
                '6.6.6.6',
                '7.7.7.7',
                '8.8.8.8',
                '9.9.9.9',
                '10.10.10.10',
            ]);

            $access = \App\ProductAccess::firstOrNew([
                'product_id' => $product_id,
                'ip' => $ip
            ]);
            $access->save();

        }
    }
}

テストデータとして用意するのは、productsproduct_accessesテーブルの2つです。

※ちなみに画像のURLには、Placeholder.com を使わせていただきました。(いつも重宝しています😉✨)

では、変更したSeederLaravelに登録して使えるようにしておきましょう。

/database/seeds/DatabaseSeeder.php

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
         // $this->call(UsersTableSeeder::class);
         $this->call(ProductsTableSeeder::class); // 👈 追加
    }
}

では最後に、以下のコマンドでデータベースを再構築します。

php artisan migrate:fresh --seed

テーブルはこうなりました。

おすすめ商品を抽出する

では、ここからが今回のメイン「この商品を見た人はこちらの商品も見ています(=おすすめ商品)」機能です。

使い勝手がいいと思いますので、今回はモデルProductの中にupdateRecommendation()というメソッドをつくります。

<?php

// 省略

class Product extends Model
{
    // 省略

    // Others
    public function updateRecommendation() {

        $id = $this->id;
        $ip_addresses = $this->accesses->pluck('ip');                   // この商品にアクセスしたIP

        // IPがアクセスした他の商品を取得 ・・・ ①
        $products = \App\ProductAccess::whereIn('ip', $ip_addresses)
            ->where('product_id', '!=', $id)
            ->get();

        // おすすめ商品のデータ加工 ・・・ ②
        $access_counts = $products
            ->groupBy('product_id')    // 商品でグループ化
            ->map(function($products){ // データ件数を返す

                return $products->count();

            })
            ->filter(function($product_count){ // データ件数が3以上のものだけにする

                return ($product_count >= 3);

            })
            ->sortDesc();

        // おすすめデータを全削除して新規登録 ・・・ ③
        $this->recommendations()->delete();

        foreach($access_counts as $product_id => $access_count) {

            $recommendation = new \App\ProductRecommendation;
            $recommendation->original_product_id = $id;
            $recommendation->product_id = $product_id;
            $recommendation->access_count = $access_count;
            $recommendation->save();

        }

        // おすすめ商品を更新した日時を変更 ・・・ ④
        $this->recommendation_updated_at = now();
        $this->save();

    }
}

この中でやっている作業を順にご紹介します。

① IPがアクセスした他の商品を取得

まず特定の商品データにアクセスしたデータをProductAccessから取得します。(おすすめするのは、他の商品データなので自分自身の除外することを忘れないでください)

② おすすめ商品のデータ加工

ここが今回で少しだけ上級コースというか、ぜひ「コレクションを使えばこんなカンジでデータ加工できるよ👍」というのを知ってほしかった部分になります。ひとつずつ見ていきましょう。

まず、groupBy()です。

->groupBy('product_id')    // 商品でグループ化

ここではDBから取得したばかりのデータ(コレクション)を商品IDごとにグループ化します。

↓↓↓ 元データをproduct_idでグループ化

そして、今回必要なのはアクセス回数(つまり関連度)なのでmap()で商品IDごとのアクセス回数に変換します。

->map(function($products){ // データ件数を返す

    return $products->count();

})

すると中身はこうなります。

さらに、アクセス回数が1回だけではおすすめ商品とはいえないので、今回はfilter()で3件以上のものだけを残すようにします。

->filter(function($product_count){ // データ件数が3以上のものだけにする

    return ($product_count >= 3);

})

結果はこれだけになりました。

そして、このデータはアクセス回数(関連度)が多い順には並んでいないので、最後にsortDesc()を使って並べ替えをしています。

すると最終的にデータはこうなりました。

これがおすすめ商品のデータになります。

③ おすすめデータを全削除して新規登録

おすすめ商品のデータを抽出したら全データを削除し、新しいデータを追加します。

④ おすすめ商品を更新した日時を変更

そして、recommendation_updated_atに現在の日時を保存し、「いつおすすめ商品を集計したか」が分かるようにしておきます。

一定期間ごとにおすすめ商品を更新できるようにする

次に、おすすめ商品の集計をある一定期間を超えていたら自動更新するためにshouldUpdateRecommendation()Productモデル内につくっておきましょう。

/app/Product.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // 省略

    public function shouldUpdateRecommendation() { // おすすめ商品を更新すべきかどうかチェック

        return (
            is_null($this->recommendation_updated_at) ||
            now()->diffInDays($this->recommendation_updated_at) >= 3 // 最後におすすめ商品を更新してから3日以上の場合
        );

    }
}

内容としては、pruductsテーブルにつくったフィールドrecommendation_updated_atをチェックして、以下2つの場合はtrueを返すようになっています。

  • まだおすすめ商品を集計してない
  • 前回の集計から(今回の場合は)3日以上経過している

使い方

では、おすすめ商品を自動的に集計する方法になります。
今回はテストとして、例えば「http://*****/product/1」にアクセスするルートを作成します。

※なお、実際はコントローラーをつくってコードを分けることをおすすめします。

/routes/web.php

Route::get('product/{product}', function(\App\Product $product){

    if($product->shouldUpdateRecommendation()) {

        $product->updateRecommendation();

    }

    $product->load('recommendations.product'); // リレーションシップを読み込み

    // ここで何か

});

中身としては、shouldUpdateRecommendation()でおすすめ商品を更新すべきかをチェックし、もし更新すべきならupdateRecommendation()を実行しています。

こうすることで、ある一定期間ごと自動的におすすめ商品を最新データにすることができます。

おすすめ商品を一覧表示してみる

では、せっかくおすすめ商品を集計できるようになったので、先ほどのルートにビューを追加してリスト表示できるようにしてみましょう。

/routes/web.php

Route::get('product/{product}', function(\App\Product $product){

    // 省略

    return view('product')->with([
        'product' => $product
    ]);

});

ビューをつくる

続いてビューです。
Vue.jsを使っていますが、シンプルに横スクロール表示しているだけです。

/resources/views/product.blade.php

<html>
<head>
    <style>

        #page-title {

            margin-bottom: 0;
            margin-left: 10px;
            color: #776;

        }

        .scroll-box {

            overflow-x: auto;
            overflow-y: hidden;
            white-space: nowrap;

        }

        .scroll-box img {

            max-height: 200px;

        }

        .scroll-item {

            display: inline-block;
            margin: 10px;

        }

    </style>
</head>
<body>
    <div id="app">
        <h3 id="page-title">この商品を見た人はこちらの商品も見ています &#x1F389;</h3>
        <div class="scroll-box">
            <a :href="'/product/'+ recommendation.product.id" class="scroll-item" v-for="recommendation in product.recommendations">
               <img :src="recommendation.product.image_url">
               <div v-text="recommendation.product.name"></div>
            </a>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                product: {!! $product !!}
            }
        });

    </script>
</body>
</html>

テストしてみる

では、ブラウザで実際に「http://*****/product/1」へアクセスしてしょう❗

アクセスするとこうなりました。

もちろん、スライドさせると別の商品が表示されます。

成功です😊👍✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ただし、マイグレーションやテストデータの用意はご自身で実行していただく必要があります。

Laravelで「この商品を見た人はこちらの商品も見ています」機能をつくる
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで今回はオンラインショップでよく見かける「この商品を見た人はこちらの商品も見ています」機能をLaravelでつくってみました。

実際のサイトでしたら、商品のカテゴリを含めて集計してもいいでしょうし、おなじやり方で「この商品を買った人はこちらの商品も買っています」機能もつくれると思います。

ぜひ皆さんもやってみてくださいね。

ではでは〜❗

「年齢のせいか、夏でも
ホットコーヒーを飲むようになりました」

このエントリーをはてなブックマークに追加       follow us in feedly