Laravel + mysql + mecab の全文検索で文書管理システムをつくる

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

さてさて、これは皆さんも感じていると思うのですが、プログラムで何かをつくるとき、解決方法は1つじゃないことが多いですよね。

送信だけとっても、HTTP送信に加えてAjaxでの送信もできますし、サーバーだってクラウドを使わずVPSを使って実装している場合も多いでしょう。

ちなみに、どの技術を使うかは「クライアントさんの状況」によって変わってくると思いますので、プログラマは「クライアントさんの希望に沿う」というコンサルティング的な要素も必要になってくると考えています。

そして、そんな選択肢の中からチョイスするものには「DBテーブルの検索」も含まれていると思います。

つまり、長い文章を高速検索できるようになる、

全文検索(Full-Text Search)

ですね。

全文検索とは、簡単に言うと「文章を前もって単語に分けておいて、その単語をから高速に検索する」という技術です。

例えば、「マトリッツォ」というキーワードで検索をする場合、通常だと全てのデータに「マトリッツォが入ってる?」の繰り返しをしないといけません。

しかし、全文検索にしておくと、「マトリッツォが入ってるのはココとココのデータです」というショートカットが用意されるので、検索が早くなるというわけです。

そこで❗

今回はLaravel + MySQL + mecabでこの「全文検索」を実装してみたいと思います。

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

※ なお、実際につくる機能は、「文書管理システム」を想定しています。

「マトリッツォって
クマが泡吹いて倒れてるように
見えません❓」

開発環境: Laravel 8.x、Ubuntu 20.04、MySQL 8.0

mecab 本体をインストールする

まずはmecab本体のインストールです。
ターミナルを開き、以下のコマンドを実行してください。

sudo apt remove mecab libmecab-dev mecab-ipadic-utf8

これでmecabがインストールされました。
なお、コマンドラインでmecabを使う方法は過去記事をご覧ください。

📝 参考記事: mecab本体のインストール

mecab プラグインをインストールする

次に、mecabMySQLで使えるようプラグインをインストールします。

※ ちなみに、mecabプラグインのインストールはどうやら環境によってパスや設定方法が違うようです。そのため、今回は私が最終的に成功した方法をご紹介しますが、おそらくUbuntu 20.04でしたらそのままでOKじゃないでしょうか。

準備

まず、mecabの設定ファイルは/etc/mecabになるのですが、どうやらこの位置にあるとmysqlから実行ができないようなので、このファイルをコピーします。

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

sudo cp /etc/mecabrc /etc/mysql/mecabrc

そして、MySQLがこのコピーされたファイルを参照できるよう設定します。
以下のコマンドを実行してください。

sudo vi /etc/mysql/my.cnf

そして、以下の部分を追加します。

[mysqld]
loose-mecab-rc-file=/etc/mysql/mecabrc
innodb_ft_min_token_size=2

innodb_ft_min_token_sizeは、「単語に区切るときに何文字以上にするか」を決定します。つまり今回のケースでは2文字以上のものを使うという意味になります。

では、設定が終わったらMySQLを再起動します。
以下のコマンドを実行してください。

sudo systemctl restart mysql

インストールを実行

続いて、プラグイン本体のインストールです。
コマンドでMySQLにログインしてください。

mysql -u root -p

※ ユーザー名は適宜変更してください。

すると、パスワードを聞かれるので入力してエンターキーを押します。(実はターミナルのパスワードでもコピペが使えますよ👍)

すると、MySQLにログインできるので以下のコマンドを実行してください。

INSTALL PLUGIN mecab SONAME 'libpluginmecab.so';

※ 最後のセミコロンも忘れずに❗

これでmecabのプラグインがインストールできました。
では、念のためそのままの状態で以下のコマンドを実行してください。

SHOW PLUGINS;

インストールされていれば以下のように表示されます。

データベースにテーブルをつくる

では、続いてLaravel側の作業になります。
以下のコマンドでモデル&マイグレーションを作成してください。

php artisan make:model Document -m

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

database/migrations/****_**_**_******_create_documents_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateDocumentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('documents', function (Blueprint $table) {
            $table->id();
            $table->string('title')->comment('タイトル');
            $table->text('body')->comment('本文');
            $table->timestamps();
        });

        // 全文検索:設定を追加
        \DB::statement('ALTER TABLE `documents` ADD FULLTEXT INDEX document_body_index (body) WITH PARSER mecab');
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        // 全文検索:設定の削除
        Schema::table('documents', function($table) {
            $table->dropIndex('document_body_index');
        });

        Schema::dropIfExists('documents');
    }
}

なお、Laravelには全文検索用のメソッドは存在していないので、SQL文を直書きで実行させています。

テストデータをつくる

次に、開発しやすいようにテストデータを追加するFactoryファイルを作成します。

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

php artisan make:factory DocumentFactory

自動でDocumentFactory.phpが作成されるので中身を以下のようにします。

database/factories/DocumentFactory.php

// 省略

public function definition()
{
    static $number = 1;

    return [
        'title' => 'テストタイトル '. $number++,
        'body' => $this->faker->realText()
    ];
}

【⚠ご注意】なお、fakerの言語設定を日本語にしていない場合は、config/app.php内にあるfaker_localeを以下のように変更しておいてください。

'faker_locale' => 'ja_JP',

では、作成したFactoryはそのままでは動きませんので、Laravel側へセットします。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

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

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

        Document::factory()->count(25)->create();

    }
}

では、これで設定は完了しましたので以下のコマンドを実行して、DBを再構築してみましょう。

php artisan migrate:fresh --seed

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

コントローラーをつくる

では、ここからは実際にブラウザで操作する部分をつくっていきます。
まずはコントローラーです。

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

php artisan make:controller DocumentController

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

app/Http/Controllers/DocumentController.php

<?php

namespace App\Http\Controllers;

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

class DocumentController extends Controller
{
    public function index()
    {
        return view('document.index');
    }

    public function list(Request $request)
    {
        $sql = 'MATCH(body) AGAINST(?)';
        $params = [$request->keyword];

        return Document::whereRaw($sql, $params)->get();
    }
}

※ ちなみに、脆弱性になりますので、以下のように直接SQL文にキーワードを埋め込むような書き方はしないでください。?を使ったプリペアドステートメントが安全です。

// ⚠ これは危険な例です!ダメ。ゼッタイ😫

$sql = 'MATCH(body) AGAINST("'. $request->keyword .'")';

ビューをつくる

では、先ほどコントローラーのindex()でセットしたビューをつくります。
以下のファイルを作成してください。

resources/views/document/index.blade.php

<html>
<head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="p-5">
    <div class="row">
        <div class="col-3">
            <input type="text" class="form-control" placeholder="キーワードを入力" v-model="keyword">
        </div>
        <div class="col-3">
            <button type="button" class="btn btn-primary" @click="search">検索する</button>
        </div>
    </div>
    <div class="row mt-3 bg-light" v-if="documents.length">
        <div class="col-12" v-text="documents"></div>
    </div>
    <div class="row mt-3 bg-light" v-else>
        検索データが見つかりません。
    </div>
</div>
<script src="https://unpkg.com/vue@3.0.11/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                keyword: '',
                documents: []
            }
        },
        methods: {
            search() {

                const url = '{{ route('document.list') }}?keyword='+ this.keyword;
                axios.get(url)
                    .then(response => {

                        this.documents = response.data;

                    });

            }
        }
    }).mount('#app');

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

内容としては、キーワードを入力し、Ajaxで送信するというシンプルなものになっています。

ルートをつくる

そして最後にルートです。
以下を追加してください。

use App\Http\Controllers\DocumentController;

// 省略

Route::get('document', [DocumentController::class, 'index'])->name('document.index');
Route::get('document/list', [DocumentController::class, 'list'])->name('document.list');

これで作業は完了です❗
お疲れ様でした😊

テストしてみる

では、「http://******/document」にブラウザでアクセスすると以下のような検索フォームが表示されます。

では、「仕事」と入力して検索してみましょう。

すると・・・・・・

はい❗

仕事」が含まれたデータを取得することができました。

では、ここで「仕事」を分割して「」で検索してみましょう。通常の検索ならこれでもデータが取得されますが、全文検索ではどうでしょうか・・・???

はい❗
データを取得することができませんでした。

これは、2文字以上でインデックスをつくっているからですね。(もちろん対象が2文字以上なので、「スコップ」や「ジョバンニ」でも検索できます)

成功です😊✨

企業様へのご提案

今回の「全文検索」機能を使うと、長い文書を保存したとしても通常の検索より高速にデータを探し出すことができ、業務の効率化につなげることができます。

もしこういった文書管理システムをご希望でしたら、ぜひお問い合わせからお気軽にご連絡ください。

どうぞよろしくお願いいたします。m(_ _)m

おわりに

ということで、今回はLaravelで全文検索をつくってみました。

なお、全文検索のデメリットとしては、「文章を単語化する際に、もし登録された単語がなければ検索には引っかからない」というものがあります。

つまり、新語や流行語には対応しにくいということになります。

とはいえ、例えば「東京スカイツリー」というテキストは、以下全てのキーワードでも検索可能なので、ある程度の融通は効かせられるかと思います。

  • 東京
  • スカイ
  • ツリー
  • スカイツリー
  • 東京スカイ
  • 東京スカイツリー

ぜひ皆さんも全文検索を有効活用してみてくださいね。

ではでは〜❗

「メカブが食べたくなってきた(笑)」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly