Searchプラグインを使ってCakePHPに検索を実装する


PHP Advent Calendar 2012の4日目を担当します@mawatarinです。前日は@wa_teradaさんでした。内容は、CakePHPのbootstrap.phpとcore.phpの翻訳です。しかも全訳!大変、有り難いですねー。

さて、今日もCakePHPネタです。過去のAdventCalendarでも、取り上げられたことのあるネタですが、その辺は気にせずに、CakeDCのSearchプラグインを使って、CakePHP製のWebアプリに簡単に検索機能を実装する方法をまとめました。目的毎に章立てしておりますので、チュートリアルとして、使っていただけると思います。
都合上、ソースコードは、各項での作業によって変更された部分のみを掲載していますが、ここで作成したアプリケーションは、各項での作業毎に分けてコミットし、GitHubで公開しておりますので、あわせて読むと分かりやすいかと思います。
Cake Blog – GitHub
Cake Blog コミットログ – GitHub

なお、ここでは、CakePHPの使い方等については、詳しくは触れません。CakePHPの基礎知識を身につけたいという方は、今ならCakePHP2実践入門という書籍がオススメです。

2013年2月5日追記
authorをautherとtypoするという恥ずかしい間違いをしていましたので、GitHubへのコミットを含め、全てを修正しました。あわせて、CakePHP2.3で作り直し、動作に問題ないことを確認しました。

Searchプラグインの何が良い?

Searchプラグインの特徴を簡単に表現するならば、以下の3つです。

  • 簡単(たった数行のコードを追加するだけ)
  • 高機能(難しいことをせずともソートやページネーションとの連動する)
  • 柔軟(標準機能で満足できない場合は、簡単に拡張できる)

私は、10月からCakePHPを勉強中の身でありまして、今回初めてSearchプラグインを使ってみたのですが、非常に簡単に満足のいく検索機能を実装することができました。
今回のサンプルアプリケーションの完成イメージは、以下の通りです。

  • 完成イメージ
    cakephp-search-plugin-05

それでは、早速、見ていきましょう。

開発環境

開発環境は以下の通りです。MAMP 2.0.5を使って開発を行いました。

ソフトウェアバージョン
PHP5.3.6
MySQL5.5.9
CakePHP2.2.4, 2.3 (2013年2月5日追記)
CakeDC Search Plugin2.1

もくじ

  1. ブログアプリの作成
    1. スキーマ
    2. CakePHPのダウンロード
    3. プラグインの設置と設定
    4. cake bake all
    5. サンプルデータの登録と、ビューの調整
  2. Search プラグインを使う
    1. 概要
      1. モデル
      2. コントローラ
      3. ビュー
    2. さまざまな検索パターン
      1. 完全一致
      2. 部分一致
      3. 前方一致、後方一致
      4. 不等号
      5. 複数フィールド
      6. 複数キーワード
      7. 複数ID
      8. その他
    3. URLを整形する
      1. 空キーを表示しないようにする
      2. BASE64エンコードする
      3. namedからquerystringに変更する
  3. まとめ

1. ブログアプリの作成

まずは、ベースとなるWebアプリとして、作者毎に記事の投稿ができる簡単なブログアプリを作りたいと思います。(ブログと呼べる代物ではないですが。)
CakePHPにCakeDC製のSearchプラグインが導入され、ブログが動作する状態になっていれば良いので、以下の通りにやる必要はありません。読み飛ばす方は、スキーマをあわせて、 bake allでOKです。
「2. Search プラグインを使う」へ移動

以下、Gitを用いた開発を前提に手順を示します。Gitを使わない場合は、Zipball等をダウンロードするなり、適宜、読み替えてください。

スキーマ

スキーマは以下の通りです。

cakephp-search-plugin-01

CakePHPのダウンロード

CakePHPのダウンロードと初期設定を行います。

想像がつく方も多いでしょうが、2行目を少し解説しておきます。
業務上、モックアップを作るということが多いので、CakePHPの初期作業をひとまとめにしています。内容は以下の通りです。個人用なので高機能ではないです。

17行目。CakePHP用の .gitignoreを毎回手作業で作ると非常に面倒なので、事前にGistに登録をしておき、cURLでファイルを取得しています。

Gist – CakePHP.gitignore
こちらのGistは、再利用していただいて構いませんが、内容が変更になる恐れもあるため、ご自身で準備されるのが良いと思います。

プラグインの設置と設定

Searchプラグインを使って検索を実装するのが目的なので、基本的にはSearchプラグインのみで構いません。
ただ、モックアップとは言え、それなりの見た目にしたいので、@slywalkerさんのTwitterBootstrapプラグインを導入しています。また、開発の補助として、DebugKitとMigrationsも導入しています。
CakePHPにMigrations Pluginを導入する

TwitterBootstrapプラグインは、CDNを利用しない場合、上記に加えて、ビルドもしくはファイルの設置作業が必要です。導入方法は以下にまとめておりますので、ここでは割愛します。
CakePHPにTwitter Bootstrapプラグインを導入する

続いて、プラグインのロード、コンポーネント、ヘルパーの設定を行います。

app/Config/core.phpSecurity.saltSecurity.cipherSeedapp/Config/database.phpの設定等は、適宜お願いします。

cake bake all

AuthorPostそれぞれを bake allします。 bakeの際に、TwitterBootstrapテンプレートを利用した場合、コントローラに app/View/Layouts/bootstrap.ctpを読み込むように設定されるので、 bootstrap.ctpを作るなり、 default.ctpを読み込むように変更するなりしてください。その際に、レイアウトでTwitter Bootstrap関連ファイルを読み込むようにしておきましょう。

サンプルデータの登録と、ビューの調整

作者のサンプルデータを3件、ブログのサンプルデータを10件登録し、bakeで生成されたコードを活かしつつ、ビューを以下の通りに修正しました。

レイアウトファイルの違いによって、微妙な差はあるにせよ、ここまでの作業で、おそらく以下のような画面が出来上がっていることでしょう。

  • cake-blog/posts
    cakephp-search-plugin-02

2. Search プラグインを使う

概要

Searchプラグインを導入することで、 Searchableビヘイビアと Prgコンポーネントを利用できるようになります。プラグインを使うための準備を行いつつ、使い方を見ていきましょう。(1章を読飛ばした方は、 CakePlugin::load等、CakePHPの初期設定をお忘れなく。)

モデル

まずは、モデルの設定です。以下のように、Postモデルを編集します。

3行目でSearchableビヘイビアの読み込み設定を行い、4行目で filterArgsプロパティを設定しています。このプロパティで、検索のタイプ等を指定します。その他にも、対象フィールドを指定する等、様々な設定を行うことができます。

指定できるタイプ
type説明
‘value’ or ‘int’等号、不等号 等の比較演算子を用いる場合に指定します。
‘like’ or ‘string’‘LIKE’を使用して、あいまい検索をする場合に指定します。
‘expression’‘BETWEEN’等でメソッドからの返り値をプレースホルダにバインドさせたい時に使います。
‘subquery’‘IN’を使用したい場合に指定します。リストを返すメソッドを指定します。
‘query’一番自由度の高いタイプです。

コントローラ

次はコントローラです。PostsControllerを以下のように編集します。

中身をみていきましょう。
2行目でPrgコンポーネントを読み込んでいます。
3行目。読んで字のごとく、Prgコンポーネントのメソッドで利用される変数の事前設定を行います。通常、配列で値を与えていきますが、こちらは省略可能です。省略する際は、trueを与えておくことで、ページまたぎ等の時に、検索フォームの値を引き継ぐことができます。具体的な使い方は後述しております。
7行目の Prg::commonProcessメソッドでは、POSTデータのバリデーションを行い、namedパラメータに変更した上で、リダイレクトしています。POST&リダイレクトの流れは、HTTPヘッダを見てみると良くわかると思います。オプションを与えることで処理を変更することも可能です(後述)。ここでいうバリデーションとは、 Searchable::validateSearchのことです。Valueが空のKeyをunsetしています。
9行目。 Searchable::parseCriteriaメソッドは、GETデータをパースして、 find('all')や、 paginateで利用できる conditionsをリターンしています。

ビュー

ビューでは特別なことをする必要はなく、単にフォームを作るだけです。

下準備は整いました。検索機能を実装していきましょう。

さまざまな検索パターン

完全一致

完全一致検索の例として、作者による絞り込みを実装してみましょう。コントローラから渡された作者一覧をビューでプルダウンとして表示しています。これにより、選択したauthor_idと完全一致したものだけを抽出できるようになります。

  • cake-blog/posts
    cakephp-search-plugin-03

部分一致

いわゆるキーワード検索として利用されることが多い使い方です。例として、タイトルフィールドを対象に部分一致検索を行っています。今回は、コントローラには手を入れる必要はありません。

前方一致、後方一致

タイトル検索を部分一致から前方一致に変更してみましょう。
beforeにfalseを、afterにtrueを与えることで、 LIKE 'searchstring%'というクエリが作られます。
後方一致の場合は、その逆です。
その他にも、一文字マッチ用のワイルドカード _(アンダースコア)も利用できます。利用方法は、 'before' => '_'といった感じです。

不等号

作成日による絞り込みを例に、不等号による検索を実装してみましょう。
filterArgsで、フィールド名を指定し、それに必要な演算子を続けるだけです。パフォーマンスが求められるのであれば、expressionタイプでのBETWEEN句の利用も検討しましょう。
ビューには、開始日と終了日を入力するテキストボックスを設置しました。実際は、jQueryのdatepickerを使って、カレンダーから選択できるようにしていますが、その解説は、ここでは割愛します。

複数フィールド

キーワード検索を複数のフィールドに対して行いたいことがあると思います。例えば、1つのキーワードで、タイトルと本文を対象に検索したいといったケースですね。部分一致で作成したものに手を加えてみましょう。
公式のREADMEの目立つ場所に、queryタイプを使った例が提示されていることもあってか、他のブログでは、それと同様に独自メソッドを実装している例が散見されましたが、これも標準機能として備わっています。以下のように、対象フィールドを配列で渡しましょう。
対象がタイトルと本文になりましたので、fieldnameをtitleからkeywordに変更しました。

複数キーワード

キーワード検索を設置するのであれば、複数キーワードによる検索も取り入れたいですよね。これも標準機能として備わっています。
connectorAnd, connectorOrにキーワードの区切りを指定しましょう。例えば、以下のような感じです。

キーワードのテキストボックスに PHP+MySQLと入力すれば、’PHP’と’MySQL’というキーワードで、AND検索が行われます。

標準機能でも、一応は要求を満たす訳ですが、一般的な使われ方を考えると、できれば、スペースでキーワードを区切りたいですよね。
その場合、connectorAndにスペースを指定すれば良い訳ですが、connectorAndとconnectorOrは、必ず同時に指定する必要がありますので、もう一方をどうするかという問題が残ります。また、全角スペースと半角スペースが区別されてしまっては、使い勝手があまりよくないので、置換処理もあった方がよいでしょう。

そこで、ビューにAND, ORを選択させるラジオボタンを設置し、モデルに整形用のメソッドを追加する方法を考えてみました。以下の通りです。

検索フォームの入力値を維持させるため、keywordは、直接加工しないようにしました。代わりにwordというキーを追加しました。(補足:サブミットした直後は維持されますが、ページ切り替えやソートをした場合に維持されず、加工した値が表示されます。)
モデルの$filterArgsプロパティから、keywordがなくなったため、このままでは、Prg::commonProcessメソッドで不必要キーと判断されてしまいunsetされます。省略していたコントローラの$presetVarsプロパティを定義しておきましょう。
詳しくは触れませんが、multipleKeywordsメソッドのユニットテストも追加しておきました。

  • cake-blog/posts
    作者を指定した絞り込み、AND, ORに対応した複数キーワード指定も可能な検索、期間を指定した検索が出来上がりました。
    cakephp-search-plugin-04

複数ID

subqueryタイプの利用例として、複数選択可能なチェックボックスによるID検索を実装してみましょう。READMEにあるサンプルを参考に、2-2-1. 完全一致で作成した作者による絞り込みを変更していきます。

説明の都合上、順序を変えて、ビュー、コントローラ、モデルと見ていきます。
まずはビューです。

リストからチェックボックスを生成するには、multipleキーにcheckboxを指定します。emptyキーは必要ありませんので削除します。また、初期状態では、全ての作者にチェックが入っている状態にしたいので、defaultキーを指定しました。
全てにチェックが入っていない状態(作者を指定しない)と、全てにチェックが入っている状態(全ての作者を指定する)は、意味合いこそ違いますが、出力結果は、同じになります。見た目としては、「全ての作者を指定する」の方が伝わりやすいかと思いましたので、初期状態で全てにチェックを入れるようにしました。
ただし、内部処理は違います。詳細は、後述します。

次にコントローラです。

$presetVarsプロパティのauthor_idキーのタイプをcheckboxに変更しました。こうすることで、通常は配列で渡ってくるところが、 Prg::commonProcessメソッドで処理が行われ、 |(パイプ)で区切られた形で渡ってくるようになります。これは必ずしも変更する必要はありません。受け取り方、URLが変わりますので、好みに応じて指定しましょう。以下に例を示します。

さて、ビューのところで触れた話題、作者を指定しない場合と全ての作者を指定する場合の内部処理を見てみましょう。作者を指定しない場合は、当然ながら、条件句は作られません。全ての作者を指定する場合は、 author_id IN (1, 2, 3)という条件句が作られます。
両者の結果は必ず同じであるので、全ての作者を指定する場合のIN句は不必要なものと言えます。そこで、author_idをunsetする処理を加え、条件句が作られないようにしました。以下の通りです。 Prg::commonProcessメソッドの前でやるか後でやるかで内容が変わってきます。前でやるとURLからもunsetされますので、作りたいアプリケーションにあわせましょう。

最後にモデルです。

author_idのタイプをvalueからsubqueryに変更し、あわせて、method, fieldキーを指定します。そうすることで、methodキーで指定したメソッドが実行されます。その結果、 fieldキーで指定したフィールド IN (methodキーで指定したメソッド実行結果)と言った条件句が生成されます。
findByAuthorsメソッドを見てみましょう。Authorsに登録されている作者一覧を取得するサブクエリを生成しています。READMEにあるサンプルでは、Containableビヘイビアの記述がありますが、今回は不要です。HABTMの時には使うことになるでしょう。

以上で完了です。

  • cake-blog/posts
    作者をチェックボックスで選択できるようになりました。
    cakephp-search-plugin-05

その他

ここまででもシンプルで十分使えるものだと思っていただけるでしょう。全ては紹介しきれませんが、最後にREADMEで紹介されていたサンプルを少し加工する形で、queryタイプの使い方を見てみたいと思います。
フィールド間のOR検索は、上記で紹介した通り標準機能で簡単に作れるのですが、AND検索は現時点では用意されていません。複数フィールドをAND検索する例です。

このメソッドは汎用性は低いので、あまり良いとは思いません。 Searchable::_addCondLikeを参考に実装したり、プラグインを拡張したり、Forkするなりして、AND検索機能を追加するのもよいでしょう。

URLを整形する

検索機能は大方できあがりました。最後に、Searchプラグインのオプションの一つ、URLの整形をみていきたいと思います。

空キーを表示しないようにする

第2章2節までに作ったもので、’PHP’というキーワードで検索をした場合、以下のようなURLになるはずです。
http://localhost/cake-blog/posts/index/author_id:/keyword:PHP/andor:and/from:/to:

機能はしているので、問題はありませんが、あまりスマートではありません。値がないキーは、表示しないようにしてみましょう。$presetVarsプロパティのemptyキーにtrueを与えることで、valueが空であった場合は表示されなくなります。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts/index/keyword:PHP/andor:and

BASE64エンコードする

‘http://localhost/’というキーワードで検索してみてください。
Not Foundが表示されるはずです。これは、以下のようなURLになることで、CakePHPの名前付きパラメータが正常に機能しなくなってしまうためです。
http://localhost/cake-blog/posts/index/keyword:http://localhost//andor:and

これを回避するため、フォームから受け取った値をBASE64エンコードしましょう。$presetVarsプロパティのencodeキーにtrueを与えます。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts/index/keyword:aHR0cDovL2xvY2FsaG9zdC8_/andor:YW5k

namedからquerystringに変更する

namedスタイルではなく、query stringとして表示させたいこともあるでしょう。以下のようにすることで変更が可能です。
Prgコンポーネントを読み込む際にcommonProcessのparamTypeにquerystringを設定します。先のコントローラの概要で解説した通り、Prg::commonProcessメソッドでnamedパラメータに変更しているところが、querystringになります。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts?keyword=PHP&andor=and

querystringに変更すると、$this->passedArgsには値が入りませんので、変更が必要です。$this->request->queryを使いましょう。

あわせて、ページネーションやソートもquerystringに変更しておきましょう。
http://localhost/cake-blog/posts?keyword=PHP&andor=and&page=2

3. まとめ

ページネーションやソートまで考慮した検索機能を独自実装しようとすると、非常に骨の折れる作業だと思います。Searchプラグインを導入することでコード量は、かなり少なくて済むでしょう。記事は、結構なボリュームになってしまいましたが、やってる内容は至ってシンプルです。まだ触れたことのない方は、ぜひチャレンジしてみてください。
CakeDC製のプラグインには、他にも、Categories, Tags, Comments, Users, OauthLibといったものがあるので、今回のように読み解きながら、サンプルブログの機能を充実させていくということをやると、勉強にもなるし面白そうだと思いました。今度、チャレンジしてみたいと思います。

それではPHP Advent Calendar 2012の5日目の@DAI199さんにバトンタッチです。
リンク:オレオレRSSをPHPで出力して読み込んでみる (PHP AdventCalender2012 5日目)