Laravel + React で検索サジェスト機能をつくる

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

さてさて、(私の中ではですが)このブログでは「ユーザビリティ」や「ホスピタリティ」をテーマにした記事を定期的にお届けしています。

というのも、実際に「おおっ、これは使いやすい」というサイトがあると、それだけでリピートする理由にプラス得点になったことがあるからです。

※ ちなみに最近だと misocaヤフーメール(ブラウザ版 in モバイル)は「使う人のことを考えてくれてるなぁ」と感じました。

そして、そう考えるとReactでもある機能をつくってみたくなりました。

それは・・・・・・

検索サジェスト機能

です。

つまり、以下のような「過去に検索したことのあるキーワード」を自動で表示&選択できる機能ですね。(しかも文字を入れるとその文字を含んでいるものだけにフィルタリングされます)

ということで、今回は「Laravel + React」で検索サジェスト機能をつくってみたいと思います。

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

8000 ポートがダメなとき、
4989(四苦八苦)を
つかいます😉👍

開発環境: Laravel 9.x、React、Vite、Inertia.js、TailwindCSS

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

まずは、DB周りの準備をします。
以下のコマンドを実行してください。

php artisan make:model Suggestion -ms

すると、「モデル」だけでなく「マイグレーション」「Seeder」3つのファイルが一気に作成されるので、中身を次のように変更します。

マイグレーション

database/migrations/****_**_**_******_create_suggestions_table.php

// 省略

public function up()
{
    Schema::create('suggestions', function (Blueprint $table) {
        $table->id();
        $table->string('key')->comment('キー');
        $table->string('word')->comment('検索ワード');
        $table->timestamps();

        $table->index('key');
    });
}

// 省略

検索ワードの件数が多い順に並べ替えたいので、keyにインデックスをつけています。

Seeder(テストデータの作成)

database/seeders/SuggestionSeeder.php

// 省略

public function run()
{
    for($i = 0; $i < 100; $i++) {

        $suggestion = new Suggestion();
        $suggestion->key = 'food';
        $suggestion->word = Arr::random([
            'ラーメン',
            'カレー',
            'ピザ',
            'パスタ',
            'ハンバーグ',
        ]);
        $suggestion->save();

    }
}

内容としては、ランダムで上記の食べ物を合計100個保存するものになっています。

Seederは作成しただけでは反映されませんので、Laravel側へ登録します。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // 省略

        $this->call(SuggestionSeeder::class); // 👈 ここを追加
    }
}

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

php artisan migrate:fresh --seed

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

(サジェスト用の)コントローラーをつくる

続いて、検索キーワードの候補データを取得するためのコントローラーをつくります。以下のコマンドを実行してください。

php artisan make:controller SuggestionController

すると、コントローラー・ファイルが作成されるので、中身を次のようにします。

app/Http/Controllers/SuggestionController.php

<?php

namespace App\Http\Controllers;

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

class SuggestionController extends Controller
{
    public function list(Request $request): array
    {
        // 検索された件数が多い順にキーワードを取得する
        $suggestions = Suggestion::query()
            ->selectRaw('word, count(*) as count')
            ->where('key', $request->key)
            ->groupBy('word')
            ->orderBy('count', 'desc')
            ->limit(10)
            ->get();

        return $suggestions->pluck('word')->toArray();
    }
}

この中では、keyで絞り込んだデータをグループ化し、件数が多い順に並べ替えるようにしています。

ちなみに今回はタイプヒントをふんだんに使ってコードを書いたのですが、そうなると、返り値をarrayにしたくなり、そうなるとこれまで書かなくてよかったtoArray()が必要になりました。(うーん、世の中の流れとはいえコードが多くなるのはちょっと…と思う今日この頃です😂 シンプルにコレクションでいいでしょうか🤔)

コンポーネントをつくる

今回、サジェストデータは「datalistタグ」で作るのですが、その部分のコンポーネントになります。

resources/js/Components/SuggestionDataList.jsx

import {useEffect, useState} from "react";

export default function SuggestionDataList(props) {

    // Data
    const listKey = props.listKey;
    const [suggestionWords, setSuggestionWords] = useState([]);

    useEffect(() => {

        if(listKey) {

            const url = route('suggestion.list');
            axios.post(url, {key: listKey})
                .then(response => {

                    setSuggestionWords(response.data)

                });

        }

    }, []);

    return (
        <datalist id={listKey}>
            {suggestionWords.map(word => (
                <option key={word} value={word} />
            ))}
        </datalist>
    );

}

この中では、表示されたらすぐに該当するデータをAjaxで取得し、そのデータを<datalist>...</datalist>にセットするようにしています。

そして、この部分が<input type="text" />と対応して候補を表示してくれるわけですね。便利❗

(検索用の)コントローラーをつくる

次に、検索用のコントローラーです。
これは今回の機能としては必須ではありませんが、テストしやすいので作成します。

php artisan make:controller SearchController

すると、コントローラー・ファイルが作成されるので、中身を変更してください。

app/Http/Controllers/SearchController.php

<?php

namespace App\Http\Controllers;

use App\Models\Suggestion;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class SearchController extends Controller
{
    public function form(): Response
    {
        return Inertia::render('Search/Form');
    }

    public function search(Request $request): array
    {
        // 検索結果が存在していた場合(今回は毎回実行)
        $has_data = true;

        if($has_data === true) { // 👈 テストなので毎回実行

            $key = 'food';
            $word = $request->word;
            Suggestion::add($key, $word); // サジェストを追加

        }

        return [];
    }
}

この中では、以下のような内容になっています。

  • form(): 検索フォーム
  • search(): 検索結果のデータ取得用(Ajax)

なお、このコントローラーはテスト向けですので毎回新しいキーワードを保存するようにしていますが、もし検索結果が見つからない場合は保存部分をスキップするような流れを想定しています。

ビューをつくる

では、SearchControllerform()でセットしたビューの中身をつくっていきましょう。

resources/js/Pages/Search/Form.jsx

import SuggestionDataList from "@/Components/SuggestionDataList";
import {useState} from "react";

export default function Form() {

    // Data
    const listKey = 'food';

    // Parameters
    const [searchWord, setSearchWord] = useState('');

    // Submit
    const handleSubmit = () => {

        const url = route('search.search');
        const params = { word: searchWord };

        axios.post(url, params)
            .then(response => {

                // ここで検索結果を処理する

            });

    };

    return (
        <div className="p-5">
            <input
                type="text"
                className="mr-2"
                placeholder="検索ワードを入力"
                list={listKey}
                value={searchWord}
                onChange={e => setSearchWord(e.target.value)} />
            <SuggestionDataList listKey={listKey} />

            <button className="bg-blue-500 text-white px-4 py-2.5 rounded" onClick={handleSubmit}>送信</button>
        </div>
    );

}

この中で重要なのが、<input><SuggestionDataList>が紐付いているという点です。

つまり、<SuggestionDataList>はコンポーネントですが、実態は<datalist>を作成するものですので、結果として通常の「input + datalist」の構成になります。

そして、その紐付けはlistKeyによって識別されるようになっています。

ルートをつくる

では、最後にルートをつくります。
これまでで作成した2つのコントローラーを以下のようにセットしてください。

routes/web.php

// 省略

// Suggestion
use App\Http\Controllers\SuggestionController;
use App\Http\Controllers\SearchController;

Route::prefix('suggestion')->controller(SuggestionController::class)->group(function(){

    Route::post('list', 'list')->name('suggestion.list');

});
Route::prefix('search')->controller(SearchController::class)->group(function(){

    Route::get('form', 'form')->name('search.form');
    Route::post('search', 'search')->name('search.search');

});

テストしてみる

では、実際にテストしてみましょう❗

Viteを起動して、ブラウザで「http://******/search/form」へアクセスしてみます。

すると以下のように表示されますので、検索ボックスにフォーカスを当ててみましょう。

すると、suggestionsテーブルから取得された「food」のキーワードが表示されました。

では、ここに新しい「食べものワード」として「干しいも」と入力し、送信ボタンをクリックしてみましょう。

ブラウザ上では何も変化はないと思いますが、裏では動いているはずなので、この状態でリロードし、再度検索ボックスにフォーカスしてみましょう。

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

はい❗
干しいも」が新しく候補に表示されました。

成功です😄✨

企業様へのご提案

今回のように、「ユーザーの操作しやすさ=(ユーザービリティ)」を向上させることは、ユーザーの滞在時間や再訪問の多さにもつながります。

もし今回のような「かゆいところに手が届く」機能をご希望でしたらいつでもお問い合わせよりご相談ください。

お待ちしております。😄✨

おわりに

ということで、今回は「Laravel + React」で検索サジェスト機能をつくってみました。

その昔はdatalistがなかったので入力のサジェスト機能をつくるのはselect2などを使う必要がありましたが、現在ではとても楽に実装できるようになりましたね。

ということで、ぜひ皆さんもやってみてくださいね。

ではでは〜❗

「ふるさと納税の返礼品、
ウマかった😄✨」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ