下記のようにウェブサイトに掲載している情報をカテゴリ・公開年・フリーワードで検索して表示する機能はよく見かけます。これをPowerCMS Xで実装する場合はどのようになるでしょうか?
検索手法の検討
PowerCMS Xの検索には2つの手法があります。
前者はデータベースをSQLで検索する機能、後者はHyper Estraierの機能によりHTMLから生成した検索インデックスを検索する機能です。今回の例ではどちらでも実装可能と考えられますが、今回は「複数の条件を指定可能なウェブサイト内検索」で話題を進めます。「複数の条件を指定可能なウェブサイト内検索」の場合はMTEntries
のようなタグを利用して記述するので、検索(フィルタ)せず一覧を表示する場合・検索(フィルタ)して一覧を表示する場合のテンプレートを共通化できます。また、「SearchEstraierプラグイン」の場合は検索インデックスに入っていない情報を出力する場合、改めてMTEntries
やMTObjectLoop
タグを利用してデータを取得する必要がありますが、「複数の条件を指定可能なウェブサイト内検索」はその必要がありません。
<mt:entries cols="title,excerpt,publication_date" limit="$object_list_limit" sort_by="publication_date" sort_order="descend">
<mt:if name="__first__">
<p class="p-search__resultCount">検索結果 : <mt:var name="object_count" escape />件</p>
<ul class="p-entryList">
</mt:if>
<li class="p-entryList__item">
掲載省略
</li>
<mt:if name="__last__">
</ul>
<mt:include module="ページネーション" />
</mt:if>
</mt:entries>
基本
複数の条件を指定可能なウェブサイト内検索のドキュメントにある通り、_filter
から始まるいくつかのパラメータと共にリクエストを行うことで検索(フィルタ)が行われます。
<form action="/search/" method="get">
<input type="hidden" name="_filter" value="entry">
<input type="hidden" name="_filter_cond_categories[]" value="ct">
<input type="text" name="_filter_value_categories[]" value="生活">
<input type="submit" value="検索">
</form>
キーワード検索の場合はパラメータquery
で指定されたキーワードによりリレーション以外の全てのカラムをLIKE検索します。スペース区切った複数のキーワードを与えることも可能です。
URLが長い・パラメータ値がそのまま渡せないケースを改良
基本で示したコードで検索を行うとどうしてもURLが長くなってしまいます。また、例のように掲載年度でフィルタする場合、選択肢が「2023年度」となっていてもパラメータの値は開始日である「2023-04-01 00:00:00」と終了日である「2024-03-31 23:59:59」という2つの値をJavaScriptで作成して渡さなければなりません。これらを解決するにはプラグインを記述する必要があります。
具体的にはpre_listingコールバック、リスト一覧を作る前の段階に独自の処理を追加し、自身で決めたパラメータに対する処理を書きます。
/**
* pre_listingコールバックでの処理(調査モデル)
*
* @param array $cb コールバックの情報
* @param Prototype $app アプリケーション
* @param array $terms 検索条件
* @param array $args 抽出条件
* @param string $extra 追加のクエリ
* @param string $ex_vals 追加のクエリにバインドする値
* @return void
*/
public function pre_listing_entry( $cb, $app, &$terms, &$args, &$extra, &$ex_vals ) {
// ここに独自の処理を記述する
}
掲載年度を_year
というパラメータで渡した場合、下記のように検索条件$terms
に公開日の始まりと終わりを指定するようにします。これでURLは_filter=entry&_year=2023
のようにすっきりします。
public function pre_listing_entry( $cb, $app, &$terms, &$args, &$extra, &$ex_vals ) {
if ( $app->id !== 'Bootstrapper' ) {
// ダイナミックパブリッシング以外の場合はreturnする。そうしないと管理画面の一覧にも効いてしまう。
return;
}
$selected_year = $app->param( '_year' );
if ( $selected_year ) {
$start_month = 4; // 開始月
$end_month = 3; // 終了月
// 開始日と終了日を算出
$start = mktime( 0, 0, 0, $start_month, 1, $year );
$end = mktime( 23, 59, 59, $end_month, date( 't', mktime( 0, 0, 0, $end_month, 1, $year + 1 ) ), $year + 1 );
// 検索条件に指定
$terms['published_on'] = ['BETWEEN' => [ $start, $end ] ];
}
}
カテゴリIDを基に検索したい
記事とカテゴリのようなリレーションデータはmt_relationテーブルに保存されています。そのため、モデル名・カラム名とカテゴリID(リレーション先ID)を基に検索を行えば該当するオブジェクトを見つけ出すことができます。このID(リレーション元ID)を$terms
に指定すると、渡されたカテゴリIDで絞り込んだ結果を取得できるので、URLは_filter=entry&category=1
のようにスッキリとします。
if ( is_array( $to_ids ) ) {
$relation_terms['to_id'] = [ 'IN' => $to_ids ];
} elseif ( strpos( $to_ids, ',' ) !== false ) {
$relation_terms['to_id'] = [ 'IN' => explode( ',', $to_ids ) ];
} else {
$relation_terms['to_id'] = $to_ids;
}
$relation_terms['from_obj'] = $from_obj;
$relation_terms['to_obj'] = $to_obj;
$relation_objects = $app->db->model( 'relation' )->load( $relation_terms, [], 'from_id' );
if ( count( $relation_objects ) ) {
foreach ( $relation_objects as $object ) {
$from_ids[] = $object->from_id;
}
}
$terms['id'] = ['IN' => $from_ids];
指定したカラムのみキーワード検索したい
先に述べたようにquery
パラメータでキーワードを渡すとリレーション以外のカラムを全て検索するため、指定したカラムのみキーワード検索する場合も自分で条件をセットする必要があります。
$freeword = $app->param('q');
$model = 'entry';
$target_columns = ['title', 'text'];
$words = explode( ',', preg_replace( '/[ ]/u', ',', $freeword ) ); // キーワードを分割
$val_stack = [];
foreach ( $words as $word ) {
$stack = [];
foreach ( $target_columns as $column ) {
$stack[] = "${model}_${column} LIKE ?";
$val_stack[] = $app->db->escape_like( $word, true, true );
}
$search_cond[] = '(' . implode( ' OR ', $stack ) . ')'; // (entry_title LIKE '%キーワード%' OR entry_text LIKE '%キーワード%') のようなクエリができあがる
}
$extra .= ' AND (' . implode( ' AND ', $search_cond ) . ')'; // (entry_title LIKE '%キーワード%' OR entry_text LIKE '%キーワード%') AND (entry_title LIKE '%キーワード2%' OR entry_text LIKE '%キーワード2%') のようなクエリができあがる
$ex_vals = array_merge( $ex_vals, $val_stack );
まとめ
いくつかのプロジェクトで「複数の条件を指定可能なウェブサイト内検索」を用いて検索を実装しましたが、基礎となるのは本稿に書いた内容で、コードを使い回しつつカスタマイズしていけば効率よく検索ページが構築できました。PowerCMS Xでの検索の実装にお悩みの方がいらっしゃいましたらご相談お待ちしております。