Laravel で「好きな芸人が似てる」マッチングシステムをつくってみる

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

さてさて、忙しいながらも早い世の中の流れに取り残されないよう、いろいろ情報チェックするようにしているのですが、その中でも個人的に好きなのが、

(システム開発の)求人情報をチェックする

ことです。

というのも、求人情報に上がってくるキーワードは「時代を反映したもの」が多いですし、たまに「えっ、そんなものがあるの!?」と、全く知らないテクノロジーを知るきっかけにもなるので、(たとえ求職者でなくても)チェックしておくのはきっと有益だと思っています。

そして、この間もいろいろと見ていて比較的数が多い求人内容だったのが、

マッチングサービスの開発

でした。

おそらくコロナ禍のご時世ですから、あまり外に出にくいのでマッチングサービスで友達や恋人を探そうというニーズが多いのかもしれません。

そこで❗

今回はLaravelを使って「趣味が合う人」をマッチングするシステムを作ってみたいと思います。

ただ、「普通の趣味」では個人的にモチベーションがあがらないので、「M-1グランプリで優勝した芸人さんで誰が好きか」でマッチングできるようにしてみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😊✨

「優勝は逃しましたが、
最近だとおいでやすこが
さんが好きです👍」

開発環境: Laravel 8.x

前提として

」と「」をマッチングさせるかという話になりますので、ログイン機能がインストールされ、テストユーザーがusersテーブルに登録されていることが前提です。

もしまだの方は以下を参考にしてみてください。

📝 参考ページ

実際のテーブルはこちら

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

では、まず初めにデータベース周りから準備していきますが、今回必要になるのは以下3つのテーブルです。

  • users: マッチングするユーザー
  • comedians: M-1グランプリ優勝した芸人さん
  • user_comedians: 「誰がどの芸人さんを好きか」を管理する中間テーブル

では、(すでにインストール済みのusers以外の)マイグレーション&モデルを作っていきましょう。以下のコマンドを実行してください。

php artisan make:model Comedian -m
php artisan make:model UserComedian -m

するとファイルが自動的に作成されるので、中身をそれぞれ以下のように変更します。

database/migrations/****_**_**_******_create_comedians_table.php

// 省略

public function up()
{
    Schema::create('comedians', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('コンビ名');
        $table->timestamps();
    });
}

そして、中間テーブルも変更してください。

database/migrations/****_**_**_******_create_user_comedians_table.php

// 省略

public function up()
{
    Schema::create('user_comedians', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->comment('ユーザーID');
        $table->unsignedBigInteger('comedian_id')->comment('芸人さんID');
        $table->timestamps();

        // 外部キー
        $table->foreign('user_id')->references('id')->on('users');
        $table->foreign('comedian_id')->references('id')->on('comedians');
    });
}

次にモデルの中身も変更します。

app/Models/User.php

// 省略

class User extends Authenticatable
{
    // 省略
    
    // Relationship
    public function comedians()
    {
        return $this->belongsToMany(
            Comedian::class,    // 結合したいモデル
            'user_comedians',   // 中間テーブル名
            'user_id',          // 自分自身(Userモデル)に対応する(中間テーブルの)フィールド
            'comedian_id'       // 取得したいテーブルに対応する(中間テーブルの)フィールド
        )->orderBy('id', 'asc');
    }
}

この中のbelongsToMany()は、いわゆる「多:多」(Many to Many)のリレーションシップにするためのものです。

なお、中間テーブルに関しては以下のページをご覧ください。

📝 参考ページ: 中間テーブルの考え方

テストデータをつくる

続いて、開発がしやすいようにSeederでテストデータをつくります。
以下のコマンドを実行してください。

php artisan make:seed ComedianSeeder
php artisan make:seed UserComedianSeeder

するとファイルが2つ作成されるので、それぞれ中身を以下のように変更します。

database/seeders/ComedianSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Comedian;
use Illuminate\Database\Seeder;

class ComedianSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $comedian_names = [
            '中川家',
            'ますだおかだ',
            'フットボールアワー',
            'アンタッチャブル',
            'ブラックマヨネーズ',
            'チュートリアル',
            'サンドウィッチマン',
            'NON STYLE',
            'パンクブーブー',
            '笑い飯',
            'トレンディエンジェル',
            '銀シャリ',
            'とろサーモン',
            '霜降り明星',
            'ミルクボーイ',
            'マヂカルラブリー',
        ];

        foreach ($comedian_names as $comedian_name) {

            $comedian = new Comedian();
            $comedian->name = $comedian_name;
            $comedian->save();

        }
    }
}

※ 敬称は省略させていただいておりますが、世の中を明るくしてくれる芸人さんたちを尊敬しております。m(_ _)m

<?php

namespace Database\Seeders;

use App\Models\Comedian;
use App\Models\User;
use App\Models\UserComedian;
use Illuminate\Database\Seeder;

class UserComedianSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $users = User::get();
        $comedians = Comedian::get();

        foreach ($users as $user) {

            $count = rand(3, 5);
            $random_comedians = $comedians->shuffle()->slice(0, $count);   // ランダムで3〜5件の芸人さんを取得

            foreach ($random_comedians as $random_comedian) {

                $user_comedian = new UserComedian();
                $user_comedian->user_id = $user->id;
                $user_comedian->comedian_id = $random_comedian->id;
                $user_comedian->save();

            }

        }
    }
}

では、SeederファイルをLaravelへ登録して有効にします。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // 省略
        $this->call(ComedianSeeder::class);      // 👈 ここを追加しました 
        $this->call(UserComedianSeeder::class);  // 👈 ここを追加しました 
    }
}

では、この状態でDBテーブルを再構築してみましょう。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

すると、実際のテーブルは以下のようになりました。

コントローラーをつくる

では、ここからは「好きな芸人が似てる」人を一覧表示する部分を作っていきます。

以下のコマンドでコントローラーを作ってください。

php artisan make:controller MatchingController

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

app/Http/Controllers/MatchingController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class MatchingController extends Controller
{
    const MIN_MATCHING_COUNT = 1;

    public function show(User $user)
    {
        $comedian_ids = $user->comedians->pluck('id');
        $matched_users = User::with(['comedians' => function($query) use($comedian_ids){ // 👈 同じ「好きな芸人さん」を取得

            $query->whereIn('comedian_id', $comedian_ids);

        }])
        ->where('id', '!=', $user->id)  // 👈 自分以外のデータを取得
        ->get()
        ->filter(function($matched_user){ // 👈 最低でも `MIN_MATCHING_COUNT` 以上マッチするものだけ

            return ($matched_user->comedians->count() >= self::MIN_MATCHING_COUNT);

        })
        ->sortByDesc(function($matched_user) { // 👈 マッチした芸人さんの数で並べ替え(降順)

            return $matched_user->comedians->count();

        });

        return view('matching')->with([
            'user' => $user,
            'matched_users' => $matched_users
        ]);
    }
}

今回の開発で一番複雑なのがこのshow()でデータを取得する部分です。

まず大きく以下のように2つのブロックに分かれると考えてください。

  • get() までの部分: DBからデータ取得する部分
  • それ以降: 取得したデータの形を変える部分

※ちなみに、見やすいコードとしては半分に分割した方がいいかもしれませんが、今回は「なんなら全部つなげることもできるよ👍」というのをご紹介したかったので、この形にしました😊✨

get()までの部分

まず、with()で結合をしているわけですが、通常では使わないfunction(){ ... }がついているかと思います。

これは、「結合先のデータにさらに条件を追加する」ためのものです。

つまり、今回の例で言うと「好きな芸人さん」が一致するデータをIDで絞り込んでいるわけです。(これがないと、好きではない芸人さんも含めたデータになってしまいます)

そして、もちろん自分自身まで含めてしまうと、フルにマッチしてしまうのでwhere('id', '!=', $user->id)で自分は省くようにします。

最後にここまでの条件でDBからデータを取得するのがget()です。

それ以降の部分

get()以降の部分は、データベースは関係なく、LaravelCollectionを使った操作になります。(なぜならモデルを使って取得したデータはCollectionになるからです)

まず、filter()は「不要なデータを省く」処理をしています。

なぜなら、データベースから取得したデータには「まったく好みがマッチしていない人」も含まれているからです。

そのため、filter()内で(今回のケースでは)「最低でも1件以上好みがマッチしている人だけ」を残すようにしています。

※ なお、件数はMIN_MATCHING_COUNTとしていつでも変更できるようにしていますが、もし他の場所でも使うならconfigフォルダ内やモデル本体に定数としてセットしておいた方がいいかもしれません。

続いて、sortByDesc()では、並べ替えを行っています。
並べ替えをする条件は、もちろん「好みがマッチした件数が多い人が先」です。

これで、「好きな芸人さんが多くマッチしている人のデータ」を取得することができました。

※ なお、今回はデータを毎回取得するようにしていますが、ユーザー数が増えると処理に時間がかかることが予想されますので、(実際の開発では)例えば「user_favorite_comedians」のようなテーブルに保存し、一定時間ごとに更新する方がいいかもしれません。

ビューをつくる

続いて、先ほどのコントローラーでセットしたビューをつくっていきましょう。

resources/views/matching.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-4">
    <h5>{{ $user->name }} さん</h5>
    <div>
        <span class="badge bg-primary">好きな芸人さん</span> {{ $user->comedians->pluck('name')->join('、') }}
    </div>
    <hr>
    <h6 class="mb-3">マッチしたユーザー</h6>
    <ul>
    @foreach($matched_users as $matched_user)
        <li class="mb-3">
            {{ $matched_user->name }} さん<br>
            <span class="badge bg-success">マッチした芸人さん</span>({{ $matched_user->comedians->count() }}件)
             {{ $matched_user->comedians->pluck('name')->join('、') }}
        </li>
    @endforeach
    </ul>
</div>
</body>
</html>

この中で少しトリッキーなのがjoin()を使った部分だと思います。

$user->comedians->pluck('name')->join('、')

ここの意味としては、以下の流れになります。

  1. ユーザーデータと結合している芸人さんデータを取得
  2. 芸人さんデータから名前だけを取得
  3. それらの名前を「、」でつないで文字列にする

ルートをつくる

では、ここまでで作ったコントローラー&ビューにアクセスできるルートを追加して完了です。

routes/web.php

use App\Http\Controllers\MatchingController;

// 省略

Route::get('matching/{user}', [MatchingController::class, 'show']);

テストしてみる

では、今回開発したマッチング機能を実際にテストしてみましょう。

まず「http://******/matching/1」(ユーザーIDが1の人のページ)にアクセスします。

すると・・・・・・

はい❗太郎さんが好きな芸人さんとマッチした人たちが表示されました。

成功です😊

では、次に「http://******/matching/2」(ユーザーIDが2の人のページ)にアクセスしてみましょう。

どうなるでしょうか・・・・・・??

はい❗こちらも芸人さんがマッチしていますね。

成功です😊✨

企業様へのご提案

今回開発したマッチング機能を使えば、以下のようなことが実現できます。

  • 共通するスキルをもった従業員たちが集まったプロジェクト・チームをつくる
  • 共通項目が多くあるお客様のグループをつくり、そこをターゲットにしたアプローチを実施する
  • 今回のように「好きな●●」だけでなく、むしろ「嫌いな●●」でマッチングすることもできる。

こういった内容の開発をお考えでしたら、ぜひお問い合わせからご連絡ください。どうぞよろしくお願いいたします。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravelを使って簡単なマッチング機能を作ってみました。

ちなみに、マヂカルラブリーの野田クリスタルさんはご自身でプログラムしたゲームでネタをしている(R-1優勝おめでとうございます❗)ので、ほぼないでしょうがお近づきになれたら、ぜひプログラムのお話を聞いてみたいもんです。

なお、プログラミング言語としては以下のようにHSPを使っているとのことでしたが、あいにくあまり馴染みがないものだったので時間ができたら一度試してみたいです。

きっと将来「野田さんを見てプログラムはじめました」っていう若手がでてくるんでしょうね。

となると、IT業界への貢献もされているということですね。
素晴らしい👍✨

ではでは〜❗

「最近のハンディクリーナーって
こんなに吸い込むんですね😳」

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