検索は巨大なトピックなので、この章全体ではSearchableと呼ばれるビヘイビアを専門に扱います。これは全文インデックス作成と検索ツールでデータベースとファイルの両方で使うことができます。
次の定義を持つNewsItemクラスを考えてみましょう:
// models/NewsItem.php
class NewsItem extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('title', 'string', 255);
$this->hasColumn('body', 'clob');
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
NewsItem:
columns:
title: string(255)
body: clob
ユーザーが異なるニュースの項目を検索できるアプリケーションを考えてみましょう。これを実装する明らかな方法はフォームを構築し投稿された値に基づいて次のようなDQLクエリを構築することです:
// test.php
// ...
$q = Doctrine_Query::create()
->from('NewsItem i')
->where('n.title LIKE ? OR n.content LIKE ?');
アプリケーションが成長するにつれてこの種のクエリはとても遅くなります。例えば%framework%パラメータで以前のクエリを使うとき、(frameworkという単語を含むタイトルもしくは内容を持つすべてのニュースの項目を見つけることと同等です)データベースはテーブルのそれぞれの列をトラバースしなければなりません。当然ながらこれは非常に遅くなります。
Doctrineはこの問題を検索コンポーネントとインバースインデックスで解決します。最初に定義を少し変えてみましょう:
// models/NewsItem.php
class NewsItem extends Doctrine_Record
{
// ...
public function setUp()
{
$this->actAs('Searchable', array(
'fields' => array('title', 'content')
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
NewsItem:
actAs:
Searchable:
fields: [title, content]
# ...
上記のモデルで生成されたSQLをチェックしてみましょう:
// test.php
// ...
$sql = Doctrine_Core::generateSqlFromArray(array('NewsItem'));
echo $sql[0] . "\n";
echo $sql[1] . "\n";
echo $sql[2];
上記のコードは次のSQLクエリを出力します:
CREATE TABLE news_item_index (id BIGINT,
keyword VARCHAR(200),
field VARCHAR(50),
position BIGINT,
PRIMARY KEY(id,
keyword,
field,
position)) ENGINE = INNODB
CREATE TABLE news_item (id BIGINT AUTO_INCREMENT,
title VARCHAR(255),
body LONGTEXT,
PRIMARY KEY(id)) ENGINE = INNODB
ALTER TABLE news_item_index ADD FOREIGN KEY (id) REFERENCES news_item(id) ON UPDATE CASCADE ON DELETE CASCADE
Here we tell Doctrine that NewsItemクラスがsearchable(内部ではDoctrineがDoctrine_Template_Searchableをロードする)として振る舞いtitleとcontentフィールドは全文検索用のインデックス付きフィールドとしてマークされます。これはNewsItemが追加もしくは更新されるたびにDoctrineは次のことを行うことを意味します:
1. インバース検索インデックスを更新するもしくは
2. インバース検索インデックスに新しいエントリを追加する(バッチでインバース検索インデックスを更新するのが効率的であることがあります)
Doctrineが使用するインバースインデックスの構造は次の通りです:
[ (string) keyword] [ (string) field ] [ (integer) position ] [ (mixed) [foreign_keys] ]
| カラム | 説明 |
|---|---|
| keyword | 検索できるテキストのキーワード |
| field | キーワードが見つかるフィールド |
| position | キーワードが見つかる位置 |
| [foreign_keys] | インデックスが作成されるレコードの外部キー |
NewsItemの例において[foreign_keys]はNewsItem(id)への外部キー参照とonDelete => CASCADE制約を持つ1つのidフィールドを格納します。
このテーブルの列のようになりますの例は次のようになります:
| キーワード | フィールド | 位置 | id |
|---|---|---|---|
| database | title | 3 | 1 |
この例において単語のdatabaseは1のidを持つNewsItemのtitleフィールドの3番目の単語です。
検索可能なレコードがデータベースにinsertされるときDoctrineはインデックスビルドのプロシージャを実行します。プロシージャが検索リスナーによって起動されているときこれはバックグラウンドで行われます。このプロシージャのフェーズは次の通りです:
1. Doctrine_Search_Analyzer基底クラスを使用してテキストを分析する
2. 分析されたすべてのキーワード用に新しい列をインデックステーブルに挿入する
新しい検索可能なエントリが追加されるときインデックステーブルを更新したくなく、むしろ特定の間隔でインデックステーブルをバッチ更新したい場合があります。直接の更新機能を無効にするにはビヘイビアを添付する際にbatchUpdatesオプションをtrueに設定する必要があります:
// models/NewsItem.php
class NewsItem extends Doctrine_Record
{
// ...
public function setUp()
{
$this->actAs('Searchable', array(
'fields' => array('title', 'content')
'batchUpdates' => true
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
NewsItem:
actAs:
Searchable:
fields: [title, content]
batchUpdates: true
# ...
更新プロシージャの実際のバッチはbatchUpdateIndex()メソッドによって起動します。これは2つのオプション引数: limitとoffsetを受けとります。バッチでインデックス化されるエントリ数を制限するためにlimitが使用できoffsetはインデックス作成を始める最初のエントリを設定するために使用できます。
最初に新しいNewsItemレコードを挿入してみましょう:
// test.php
// ...
$newsItem = new NewsItem();
$newsItem->title = 'Test';
$newsItem->body = 'test';
$newsItem->save();
バッチ更新を有効にしない場合NewsItemレコードを挿入もしくは更新するときにインデックスは自動的に更新されます。バッチ更新を有功にする場合次のコードでバッチ更新を実行できます:
// test.php
// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$newsItemTable->batchUpdateIndex();
デフォルトではDoctrineはテキスト分析のためにDoctrine_Search_Analyzer_Standardを使用します。このクラスは次のことを実行します:
Doctrine_Search_Analyzer_Interfaceを実装することで独自のアナライザークラスを書くことができます。MyAnalyzerという名前のアナライザーを作成する例は次の通りです:
// models/MyAnalyzer.php
class MyAnalyzer implements Doctrine_Search_Analyzer_Interface
{
public function analyze($text)
{
$text = trim($text);
return $text;
}
}
検索アナライザーはanalyze()という名前の1つのメソッドを持たなければなりません。このメソッドはインデックス化される入力テキストの修正版を返します。
このアナライザーは検索オブジェクトに次のように適用されます:
// test.php
// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$search = $newsItemTable
->getTemplate('Doctrine_Template_Searchable')
->getPlugin();
$search->setOption('analyzer', new MyAnalyzer());
Doctrine_SearchはApache Luceneに似たクエリ言語を提供します。Doctrine_Search_Queryは人間が読解でき、構築が簡単なクエリ言語を同等の複雑なDQLに変換します。そしてこのDQLは通常のSQLに変換されます。
次のコードはレコードのidと関連データを読み取るシンプルな例です。
// test.php
// ...
$newsItemTable = Doctrine_Core::getTable('NewsItem');
$results = $newsItemTable->search('test');
print_r($results);
上記のコードは次のクエリを実行します:
SELECT
COUNT(keyword) AS relevance,
id
FROM article_index
WHERE id IN (SELECT
id
FROM article_index
WHERE keyword = ?)
AND id IN (SELECT
id
FROM article_index
WHERE keyword = ?)
GROUP BY id
ORDER BY relevance DESC
コードの出力は次の通りです:
$ php test.php
Array
(
[0] => Array
(
[relevance] => 1
[id] => 1
)
)
実際のNewsItemオブジェクトを読み取るために別のクエリでこれらの結果を使うことができます:
// test.php
// ...
$ids = array();
foreach ($results as $result) {
$ids[] = $result['id'];
}
$q = Doctrine_Query::create()
->from('NewsItem i')
->whereIn('i.id', $ids);
$newsItems = $q->execute();
print_r($newsItems->toArray());
上記の例は次の出力を生み出します:
$ php test.php
Array
(
[0] => Array
(
[id] => 1
[title] => Test
[body] => test
)
)
オプションとして検索インデックスを使用して結果を制限するwhere条件サブクエリで修正するためにsearch()メソッドにクエリオブジェクトを渡すことができます。
// test.php
// ...
$q = Doctrine_Query::create()
->from('NewsItem i');
$q = Doctrine_Core::getTable('Article')
->search('test', $q);
echo $q->getSqlQuery();
上記のgetSql()の呼び出しは次のSQLクエリを出力します:
SELECT
n.id AS n__id,
n.title AS n__title,
n.body AS n__body
FROM news_item n
WHERE n.id IN (SELECT
id
FROM news_item_index
WHERE keyword = ?
GROUP BY id)
クエリを実行してNewsItemオブジェクトを取得できます:
// test.php
// ...
$newsItems = $q->execute();
print_r($newsItems->toArray());
上記の例は次の出力を生み出します:
$ php test.php
Array
(
[0] => Array
(
[id] => 1
[title] => Test
[body] => test
)
)
前に述べたようにDoctrine_Searchはファイル検索にも使うことができます。検索可能なディレクトリを用意したい場合を考えてみましょう。最初にDoctrine_Search_Fileのインスタンスを作る必要があります。これはDoctrine_Searchの子クラスでファイル検索に必要な機能を提供します。
// test.php
// ...
$search = new Doctrine_Search_File();
2番目に行うことはインデックステーブルを生成することです。デフォルトではDoctrineはデータベースのインデックスクラスをFileIndex
と名づけます。
上記のモデルによって生成されたSQLをチェックしてみましょう:
// test.php
// ...
$sql = Doctrine_Core::generateSqlFromArray(array('FileIndex'));
上記のコードは次のSQLクエリを出力します:
CREATE TABLE file_index (url VARCHAR(255),
keyword VARCHAR(200),
field VARCHAR(50),
position BIGINT,
PRIMARY KEY(url,
keyword,
field,
position)) ENGINE = INNODB
Doctrine_Core::createTablesFromArray()メソッドを使用することでデータベースで実際のテーブルを作ることができます:
// test.php
// ...
Doctrine_Core::createTablesFromArray(array('FileIndex'));
ファイルサーチャーを使い始めることができます。この例ではmodelsディレクトリのインデックスを作りましょう:
// test.php
// ...
$search->indexDirectory('models');
indexDirectory()はディレクトリを再帰的にイテレートしインデックステーブルを更新しながらその範囲のすべてのファイルを分析します。
最後にインデックス化されたファイルの範囲内でテキストのピースの検索を始めることができます:
// test.php
// ...
$results = $search->search('hasColumn');
print_r($results);
上記の例は次の出力を生み出します:
$ php test.php
Array
(
[0] => Array
(
[relevance] => 2
[url] => models/generated/BaseNewsItem.php
)
)
Searchableビヘイビアのすべてを学んだのでHierarchical Dataの章でNestedSetビヘイビアの詳細を学ぶ準備ができています。NestedSetはSearchableビヘイビアのように大きなトピックなので1つの章全体で扱います。