ククログ

株式会社クリアコード > ククログ > LaravelでPostgreSQLとPGroongaを使って日本語全文検索を実現する方法

LaravelでPostgreSQLとPGroongaを使って日本語全文検索を実現する方法

はじめに

※この記事は、Laravelを使った開発の経験がある人を対象としています。Laravelの基本的な使い方自体の説明は含まれていませんので、ご注意下さい。

Webアプリケーションを開発していると、「検索窓に入力された語句を含むレコードを一覧表示する」といった機能を付けたくなる場面がよくあります。この時よく使われる手軽な方法としてSQLのLIKEがありますが、LIKEには検索対象のレコードの数が増えれば増えるほど処理に時間がかかるという欠点があります。そこで登場するのが全文検索という手法です。全文検索では事前に用意しておいたインデックス情報を使うことにより、レコード数が増加しても安定して高速な検索を行うことができます。サービスの利用者が増加して数万・数十万といった数のレコードを取り扱うような必要が生じてきた場合、全文検索の導入を検討する価値は十分にあるでしょう。

以前、Ruby on Railsで作ったアプリケーションからPGroongaを使って日本語全文検索機能を実現する方法を紹介しました。今回はそのPHP版として、Laravelで作ったブログ風のアプリケーションにPGroongaを使った日本語全文検索機能を組み込む方法をご紹介します。

この記事では、適当なLaravelアプリケーションが手元に無い場合を想定し、解説用に用意したLaravelアプリケーションとPostgreSQLとの組み合わせに対して、全文検索機能を組み込む手順を解説しています。すでに開発中・運用中のPostgreSQLを使用したアプリケーションがある場合には、そのアプリケーションに組み込む手順としてテーブル名等を適宜読み替えて下さい。

また、タイトルにも書いてある通りですが、この記事で紹介しているPGroongaは、PostgreSQLに対して全文検索機能を提供するソフトウェアです。開発・運用中のアプリケーションがPostgreSQL以外のデータベースを使用している場合にはPGroongaを使えませんので、くれぐれもご注意下さい。(他のデータベースを使っている場合、例えばMySQLやMariaDBであれば、PGroongaの代わりにMroongaを使うことになります。その場合の手順はここでは解説していませんので、あしからずご了承ください。)

開発環境の準備

それでは、開発環境を用意していきます。まず、以下の2つを用意します。

  • Homesteadを動作させるホストマシン(ここではUbuntu 16.04LTSと仮定します)

  • 動作確認用のWebブラウザが動作するクライアント

ホストマシンとしてLinuxのデスクトップ環境を用意して、ホストマシン自身をクライアントとして使うのが最も簡単です。別々のマシンを使う場合には、両者は同じネットワーク上に存在するか、もしくはホストマシンのネットワーク上の任意のコンピュータにクライアントから自由に接続できるものとします。

Laravelには開発環境を簡単に構築するためのHomesteadという枠組みがあり、今回はそれを使ってみることにします。Vagrantが必要なので、事前にホストマシンにVagrant 1.9以上VirtualBoxをインストールしておいて下さい。

VMの準備(ホストマシン上の操作)

開発環境のboxイメージが公開されているので、まずはそれを導入します。

% vagrant box add laravel/homestead
==> box: Loading metadata for box 'laravel/homestead'
box: URL: https://atlas.hashicorp.com/laravel/homestead
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.

1) parallels
2) virtualbox
3) vmware_desktop

ここではVirtualboxのイメージを使うので2を選択します。

Enter your choice: 2
==> box: Adding box 'laravel/homestead' (v2.1.0) for provider: virtualbox
box: Downloading: https://atlas.hashicorp.com/laravel/boxes/homestead/versions/2.1.0/providers/virtualbox.box
==> box: Successfully added box 'laravel/homestead' (v2.1.0) for 'virtualbox'!
vagrant box add laravel/homestead 14.75s user 7.42s system 19% cpu 1:53.24 total

boxを正常にダウンロードできたので次に進みます。

Homesteadの準備(ホストマシン上の操作)

Homesteadのリポジトリを以下のようにホストの作業ディレクトリにcloneします。

% mkdir ~/work
% cd ~/work
% git clone https://github.com/laravel/homestead.git

masterブランチは不安定な場合があるため、今回はリリースブランチを使います。

% cd homestead
% git checkout v5.4.0
...
HEAD is now at f54a9f0... Tagging 5.4.0 (#595)
(END):

Homestead.yamlの準備(ホストマシン上の操作)

ホストにて init.sh を実行します。

% bash init.sh
Homestead initialized!

すると以下の3つのファイルが同じディレクトリに作成されます。

  • Homestead.yaml

  • after.sh

  • aliases

Homestead.yaml はVMの設定ファイルです。

今回は以下の項目を変更します。

  • ip: 異なるネットワークのIPアドレスを指定する。(ホストマシンが接続しているネットワークが192.168.10.0/24なら、192.168.20.0/24などのアドレスにする)

  • authorize: ホストマシンで作成したsshの公開鍵のパスを指定する。

  • keys: ホストマシンで作成したsshの秘密鍵のパスを指定する。

  • folders: ホストマシンのディレクトリーをゲスト上から見えるようにする指定だが、ホストマシン上にはLaravelアプリケーション用のファイルを設置しないため不要なので、コメントアウトする。

  • sites: ゲスト上にこれから作成するアプリケーションに合わせてパスを書き換える。

  • networks: ゲストがホストマシンが接続しているネットワークにブリッジ接続するための設定を追加する。

上記変更を反映した Homestead.yaml は次の通りです。

---
ip: "192.168.20.10" # ホストマシンと異なるネットワークになるようにする。
memory: 2048
cpus: 1
provider: virtualbox

authorize: ~/id_homestead.pub #公開鍵のパス

keys:
    - ~/id_homestead #秘密鍵のパス

#folders:
#    - map: ~/work/Blog
#      to: /home/vagrant/Blog

sites:
    - map: homestead.app
      to: /home/vagrant/Blog/public # 後々、この位置にファイルが作られる。

databases:
    - homestead

networks:
    - type: "public_network"
      bridge: "eth0" # "wlan0"など、ホストマシンの主なインターフェースに合わせる。

IPアドレスやブリッジのインタフェース名、鍵の情報は環境に応じて適宜読み替えてください。

VMの起動(ホストマシン上の操作)

設定ファイルとコマンドの準備ができたので、ホストマシンからVMを起動します。

% vagrant up

設定に問題なければVMが正常起動します。 続けて、クライアントからゲスト上のLaravelアプリケーションに接続するために、起動したVMのブリッジ接続でのIPアドレスを調べます。

% host_if=eth0
% host_nw="$(ip addr | grep -o "inet.*$host_if" | cut -d ' ' -f 2 | cut -d '.' -f -3)."
% vagrant ssh -- ip addr | grep "$host_nw"
    inet 192.168.10.52/24 brd 192.168.10.255 scope global enp0s9

この例では192.168.10.52が割り当てられているので、クライアントからは http://192.168.10.52/ をブラウザで開けば動作を確認できることになります。

hostsファイルの編集(クライアント上の操作)

http://192.168.10.52/のようなIPアドレスの直接入力は煩雑なので、クライアントのhostsファイルを編集して、homestead.appというホスト名で参照できるようにしておきます。クライアントのhostsに以下の内容を書き足しましょう。

192.168.10.52 homestead.app

クライアントがUbuntuのデスクトップ環境の場合、hostsは /etc/hostsです。クライアントがWindowsの場合は、hostsは C:\windows\system32\drivers\etc\hosts の位置にあります。

以上で準備完了です。クライアント上のWebブラウザで http://homestead.app/ を開いてみて下さい。以下のようなエラーページが表示されるはずです。

(エラーページのスクリーンショット)

これはHomestead上のnginxが返しているエラーで、所定の位置にまだLaravelアプリケーションが存在していないという事を示しています。

Laravelアプリケーションのセットアップ

それではLaravelによるブログ風のアプリケーションを用意していきます。といっても、Laravelでのブログ風アプリケーションの開発の仕方そのものはここでは重要ではないので、あらかじめこちらで用意したサンプルのブログ風アプリケーションを使うことにしましょう。

まず、ホストマシンからゲストマシンへsshでログインします。

% vagrant ssh
vagrant@homestead:~$

そうしたら、サンプルアプリケーションのGitリポジトリをcloneします。この時、clone先のディレクトリ名をBlogと明示して、ファイルの設置先がホストマシン上のHomestead.yamlの内容と一致するようにする必要があることに気をつけて下さい。

vagrant@homestead:~$ git clone https://github.com/clear-code/pgroonga-example-laravel.git Blog

cloneし終えたら、アプリケーションを初期化します。

vagrant@homestead:~$ cd Blog
vagrant@homestead:~/Blog$ composer install
vagrant@homestead:~/Blog$ php artisan migrate
vagrant@homestead:~/Blog$ php artisan db:seed

サンプルアプリケーションの仕様

以上の手順をすべて実施したら、クライアントから http://homestead.app/posts を開いてみて下さい。以下のような記事一覧ページが表示されるはずです。

(サンプルアプリケーションのスクリーンショット)

Homestead.yamlの設定(例えば、Laravelのアプリケーションの設置先パス)を間違えていると、先のエラーページと同じ物が表示されるかもしれません。その場合、Homestead.yamlを修正してから vagrant provision を実行し、ゲスト上のnginx等を再起動する必要があります。

記事一覧ページの右上にある検索窓に「Groonga」のようなキーワードを入力すると、そのキーワードの検索結果として、postsテーブルのレコードのうちbodyカラムにキーワードを含むレコードの一覧が表示されます。ただ、コントローラの実装(app/Http/Controllers/PostController.php)を見ると分かりますが、これは以下のような単純なSQLのLIKEによる絞り込みの結果です。Groonga OR Mroongaのような凝った検索は行えません。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
  public function index(Request $request)
  {
    $query = $request->get('query');
    if (!empty($query)) {
      $posts = \App\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
    }
    else {
      $posts = \App\Post::orderBy('id', 'desc')->get();
    }
    return \View::make('posts.index')
             ->with('posts', $posts)
             ->with('query', $query);
  }
}

注意点

Homesteadの環境で普通にlaravel new Blogすると、MySQLを使うように設定されたLaravelアプリケーションが作られます。しかし今回はPGroongaの使い方の解説なので、この例では以下のように.envを編集して、データベースにはPostgreSQLを使うよう設定してあります。

commit 5f48f346ccf36e07aefdf062b2f374d83dc6151d
Author: YUKI Hiroshi <yuki@clear-code.com>
Date:   Mon Jun 26 01:56:56 2017 +0000

    Use PostgreSQL by default

diff --git a/.env b/.env
index f41ce19..ceaa34d 100644
--- a/.env
+++ b/.env
@@ -5,9 +5,9 @@ APP_DEBUG=true
 APP_LOG_LEVEL=debug
 APP_URL=http://localhost
 
-DB_CONNECTION=mysql
+DB_CONNECTION=pgsql
 DB_HOST=127.0.0.1
-DB_PORT=3306
+DB_PORT=5432
 DB_DATABASE=homestead
 DB_USERNAME=homestead
 DB_PASSWORD=secret
diff --git a/config/database.php b/config/database.php
index cab5d06..abf3d43 100644
--- a/config/database.php
+++ b/config/database.php
@@ -13,7 +13,7 @@ return [
     |
     */
 
-    'default' => env('DB_CONNECTION', 'mysql'),
+    'default' => env('DB_CONNECTION', 'pgsql'),
 
     /*
     |--------------------------------------------------------------------------

冒頭にも述べていますが、PGroongaはPostgreSQLに対して全文検索機能を提供する物なので、それ以外のデータベースに対しては使えません。ご注意下さい。

PGronngaによる全文検索機能の組み込み(ゲスト上での作業)

お待たせしました! ようやくここからが本題です。

すでにあるLaravelアプリケーションでPGroongaを使って全文検索をするには、以下の4つのステップを踏みます。

  1. PGroongaのインストール

  2. PGroongaの有効化

  3. インデックスの作成

  4. PGroongaを使って検索するように問い合わせ部分を変更

それでは順番に見ていきましょう。

PGroongaのインストール

何はともあれPGroongaのインストールが必要です。Homesteadの環境はUbuntu 16.04ベースということで、今回はUbuntu用のインストール手順を参照しました。以下は実際に実行したコマンドです。

$ sudo add-apt-repository -y universe
$ sudo add-apt-repository -y ppa:groonga/ppa
$ sudo apt-get update
$ sudo apt-get install -y -V postgresql-9.5-pgroonga

PGroongaのインストール手順は実行環境によって異なります。Ubuntu 16.04以外の環境でのインストール手順については、PGroongaのプロジェクトサイトで公開されているインストール手順の説明を参照して下さい。

PGroongaの有効化

PGroongaは、サーバー上にパッケージをインストールしただけでは使えません。機能を利用するには、PostgreSQLのデータベースに対してCREATE EXTENSION pgroonga;というSQL文を明示的に実行する必要があります。

ということで、このSQL文を実行するためのマイグレーションを作成します。

$ php artisan make:migration install_pgroonga
Created Migration: 2017_06_23_091529_install_pgroonga
<?php

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

class InstallPgroonga extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
      // ここから追記
      DB::statement('CREATE EXTENSION pgroonga');
      // ここまで追記
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
      // ここから追記
      DB::statement('DROP EXTENSION pgroonga CASCADE;');
      DB::statement('DELETE FROM pg_catalog.pg_am WHERE amname = \'pgroonga\';');
      // ここまで追記
    }
}

upに書かれている内容はPGroongaのインストール手順にあるもので、downに書かれている内容はアンインストール手順にあるものです。

用意ができたら、このマイグレーションを実行します。

$ php artisan migrate

これでPGroongaを使う準備ができました。

検索対象のカラムに全文検索用のインデックスを作成する

次に、PGroongaで全文検索するためのインデックスを作ります。

Laravelのマイグレーションではindexメソッドでインデックスを定義します。

$ php artisan make:migration add_posts_body_index
Created Migration: 2017_06_23_091530_add_posts_body_index
<?php

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

class AddPostsBodyIndex extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
      // ここから追記
      Schema::table('posts', function($table) {
        $table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
      });
      // ここまで追記
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
      // ここから追記
      Schema::table('posts', function($table) {
        $table->dropIndex('pgroonga_body_index');
      });
      // ここまで追記
    }
}

PGroongaを正しく使えているという事を説明するために、PGroongaのチュートリアルで説明されている内容との対応関係を見ていきます。 このチュートリアルでの説明に倣うと、インデックス作成用のSQL文は以下のようになります。

CREATE INDEX pgroonga_body_index ON posts USING pgroonga (id, body);

ここで、pgroonga_body_indexはPGroongaでの慣習に倣って付けた任意のインデックス名、postsはテーブル名です。USING pgroonga (id, body)は、インデックスの種類としてHashやB-treeなどと同列の物としてPGroongaを選択し、インデックスにはid(主キー)とbodyの両方を含めるという意味になっています。

  Schema::table('posts', function($table) {
    $table->index(['id', 'body'], 'pgroonga_body_index', 'pgroonga');
  });

Laravelのマイグレーションでは、テーブル名はSchema::table('posts'...の部分で示されています。indexメソッドでインデックスを作成しており、この第1引数の['id', 'body']がインデックスに含めるカラム名の配列、第2引数がインデックス名、第3引数がインデックスの種類を示しています。PGroongaのチュートリアルで説明されている情報が過不足無く指定できていることが分かるでしょう。

なお、インデックス名を省略してnullにするとインデックス名を自動的に決定させることもできますが、downのマイグレーションでインデックス名を指定してdropIndexメソッドを呼ぶ都合上、ここではインデックス作成時にも明示的にインデックス名を指定しています。

そして、マイグレーションを実行します。

$ php artisan migrate

これによって、postsテーブルのbodyカラムを対象とした全文検索用のインデックスが作成されます。既存のレコードに対するインデックスもこの時一緒に作成されますし、この後で作成されたレコードに対しても自動的にインデックスが作成されるようになります。

以上で、全文検索の準備が整いました。

PGroongaを使って検索するように問い合わせ部分を変更

最後に仕上げとして、全文検索にPGroongaを使うように検索処理を書き換えます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostsController extends Controller
{
  public function index(Request $request)
  {
    $query = $request->get('query');
    if (!empty($query)) {
      // 変更前
      // $posts = \App\Models\Post::where('body', 'like', "%{$query}%")->orderBy('id', 'desc')->get();
      // 変更後
      $posts = \App\Models\Post::whereRaw('body @@ ?', $query)->orderBy('id', 'desc')->get();
    }
    else {
      $posts = \App\Models\Post::orderBy('id', 'desc')->get();
    }
    return \View::make('posts.index')
             ->with('posts', $posts)
             ->with('query', $query);
  }
}

PGroongaで全文検索をするためには、PGroonga専用の演算子を使います。whereメソッドではそれらの演算子を取り扱えないので、ここではwhereRawメソッドを使ってSQLの式を直接書いています。

今回使った@@という演算子(この演算子はPGroongaの現在のドキュメントでは&?で置き換えられていることになっていますが、&?PHPの実装上の都合で使えないため、非推奨のこちらを使用しています)は、与えられた検索クエリを一般的なWebの検索エンジンの検索クエリのように取り扱う物です。よって、Groonga リリース(Groongaとリリースの両方の語を含む)やGroonga OR PGroonga(GroongatoPGroongaのどちらか片方だけでも含む)や( Mroonga OR PGroonga ) リリース(MroongaかPGroongaのどちらか片方に加えて、リリースという語句を含む)のような複雑なクエリも、検索窓に入力するだけでそのまま使うことができます。

(複雑なクエリで検索した状態のスクリーンショット)

以上で、PGroongaによる全文検索への乗り換えが完了しました。実際に検索を実行して、期待通りの結果が返ってくるか確かめてみて下さい。

まとめ

以上、LaravelアプリケーションでPGroongaを使って全文検索を行う手順の語句最初のステップをご紹介しました。

この解説を見ると分かる通り、すでにPostgreSQLを使っている環境であれば、PGroongaを使い始めるのは非常に簡単です。また、アプリケーション内の変更はごく一部だけで済むため、LIKE検索との性能比較もやりやすいでしょう。全文検索を使ったことがない人は、これを機にぜひ一度試してみて下さい。