前回まででAtomPubの機能をすべて実装しました。今回はAtomPubの拡張仕様であるGoogle Data APIs
全文検索にはHyper Estraierというオープンソースの検索エンジンを用います。Hyper Estraierはフレーズ検索や属性検索をサポートしているため、
サンプルコードはこちらからダウンロードできます。
GDataとは
GDataはGoogle CalendarやBloggerなどのサービスを利用するためのAPIです。AtomPubの拡張仕様として定義されています。GDataは、
なお、
検索クエリ
GDataでは以下のパラメータが定義されています。
パラメータ | 説明 |
q | 全文検索文字列、 |
author | エントリの著者 |
alt | 代替表現 |
callback | コールバック関数名 |
updated-min,updated-max | エントリ更新日時の範囲 |
published-min,published-max | エントリ発行日時の範囲 |
start-index | 取得した結果のうち、 |
max-results | 取得する結果の最大数 |
全文検索
著者検索
代替表現
updated-minなどのパラメータを使って、
クエリの例を示します。日本時間で2008年1月1日以降のatompubを含むエントリを検索する場合は、
http://localhost:3000/entrycollection?updated-min=2008-01-01T00:00:00+09:00&q=atompub
カテゴリを検索するときは、
パラメータ | 説明 |
/-/ |
カテゴリフィルタ |
メンバリソースのURIと区別するために、
たとえば、
http://localhost:3000/entrycollection/-/perl|ruby
JSONレスポンス
代替表現パラメータ
属性は文字列プロパティに変換
XML
<link href="http://example.com/collection" rel="self"/>
JSON
{ "link": { "href": "http://example.com/collection",
"rel" : "self" } }
子要素はオブジェクトプロパティに変換(テキスト要素のキーは"$t")
XML
<author>
<name>Foo Bar</name>
<email>foo@example.com</email>
</author>
JSON
{ "author": { "name" : { "$t": "Foo Bar" },
"email": { "$t": "foo@example.com" } } }
複数存在する要素は配列プロパティに変換
XML
<link href="http://example.com/"/>
<link href="http://example.com/collection" rel="self"/>
JSON
{ "link": [ { "href": "http://example.com/" },
{ "href": "http://example.com/collection", "rel": "self" } ] }
Catalysltアプリケーションの作成
新しいWebアプリケーションとして実装します。GDataというWebアプリケーション
% catalyst.pl GData % cd GData/
Hyper Estraier のセットアップ
まず、
Hyper Estraierでは、
属性名 | 説明 |
uri | リソースのURI |
category | カテゴリ |
author | 著者名と電子メールアドレス |
updated | updated要素のUNIX time |
published | published要素のUNIX time |
xml | エントリの文字列 |
Hyper Estraierのデータベースを作成し、
GData % estmaster init test.db GData % estmaster start -bg test.db # -bg: バックグラウンドで起動する
Hyper Estraierでは
次に設定ファイルを修正しますが、
GData % estmaster stop test.db
設定ファイルを修正します。設定ファイルはtest.
# attribute indexes (attribute name and data type)
attrindex: @uristr
attrindex: @categorystr
attrindex: @authorstr
attrindex: @updatednum
attrindex: @publishednum
設定ファイルからattrindexという文字列を検索し、
設定ファイルの編集が終わったら、
GData % estmaster start -bg test.db
モデルの作成
モデルを実装します。モデルには、
GData % perl script/gdata_create.pl model Entries Estraier http://localhost:1978/node/entries admin admin
コマンドの引数は次の通りです。
引数 | 説明 |
model | 作成対象 |
Entries | 作成するクラス名 |
Estraier | スーパークラス名 |
http:// |
Hyper EstraierノードのURI |
admin | Hyper Estraierのユーザ名 |
admin | Hyper Estraierのパスワード |
コレクションコントローラの実装
ヘルパスクリプトを使ってコントローラクラスのひな形を作成します。
GData % perl script/gdata_create.pl controller EntryCollection Atompub::Collection
第1回、
メンバの列挙(List)
まず、
use Search::Estraier;
sub get_feed :Atompub(list) {
my($self, $c) = @_;
# 検索条件オブジェクトを構築する
my $cond = Search::Estraier::Condition->new;
$cond->set_options(qw(SIMPLE)); # 簡便書式に設定
$cond->add_attr('@updated NUMGE 0'); # updatedが0以上
$cond->set_order('@updated NUMD'); # updatedの降順にソート
まず、
検索クエリに簡便書式を指定します。簡便書式についてはHyperEstraierのドキュメントを参照してください。
デフォルトの検索条件として
更新日時の新しい順にソートします。厳密には、
全文検索クエリの解析
# 全文検索クエリの解析
if (my $q = $c->req->param('q')) {
$q =~ s/\bOR\b/|/g; # ORを|に置換する
$q =~ s/\s+-/ ! /g; # キーワード頭の-を!に置換する
$cond->set_phrase($q);
}
全文検索クエリを解析します。GDataは"OR"という文字列でOR検索を指定しますが、
カテゴリクエリの解析
# カテゴリクエリの解析
my @args;
if ((@args = @{ $c->req->args }) > 2 && $args[1] eq '-') {
$args[2] =~ s/\|/ /g ?
$cond->add_attr("\@category ISTROR $args[2]")
: $cond->add_attr('@category ISTRAND '.join(' ', @args[ 2..$#args ]));
}
カテゴリ検索クエリを解析します。カテゴリはパスで指定されるので、
# 著者クエリの解析
if (my $author = $c->req->param('author')) {
$cond->add_attr("\@author ISTRAND $author");
}
著者クエリを解析します。AND検索
# 日時クエリの解析
my @t = ([qw(updated min NUMGE)], [qw(updated max NUMLT)],
[qw(published min NUMGE)], [qw(published max NUMLT)],);
for (@t) {
if (my $t = $c->req->param("$_->[0]-$_->[1]")) {
$cond->add_attr(join ' ', '@'.$_->[0], $_->[2], $t);
}
}
日時クエリを解析します。NUMGEとNUMLTは数値比較を行うHyperEstraierの属性検索式で、
# 取得結果の開始位置と最大数の解析
$cond->set_skip($c->req->param('start-index')-1)
if $c->req->param('start-index');
$cond->set_max($c->req->param('max-results'))
if $c->req->param('max-results');
取得結果の開始位置と最大数を解析します。GDataはエントリを1から数えますが、
# 検索の実行
my $rs = $c->model('Entries')->search($cond, 0);
検索を実行します。searchメソッドの第2引数は、
# フィード(XML::Atom::Feed) のひな型
my $feed = $self->collection_resource->body;
# フィードのひな形にエントリを追加する
for my $i (0 .. $rs->doc_num-1) {
my $doc = $rs->get_doc($i);
my $entry = XML::Atom::Entry->new(\$doc->attr('@xml'));
$feed->add_entry($entry);
}
return 1;
}
フィードのひな型を取得し、
最後にtrueを返して終了です。
なお、
メンバの追加と更新(Create,Update)
エントリの追加
sub do_update_or_create_entry :Atompub(create, update) {
my($self, $c) = @_;
# エントリ本体を取得する(XML::Atom::Entry)
my $entry = $self->entry_resource->body;
# エントリ本文
my $xml = $entry->as_xml;
$xml =~ s/[\r\n]+/ /g; # 改行文字を除去する
# category 要素
my $category = join ' ', map { '{'.($_->scheme || '').'}'.$_->term } $entry->category;
# author の子要素
my $author = join ' ', grep { defined } map { ($_->name, $_->email) } $entry->author;
# 文書オブジェクトに属性と本文を設定する
my $doc = Search::Estraier::Document->new;
$doc->add_attr('@uri', $self->entry_resource->uri);
$doc->add_attr('@category', $category) if $category;
$doc->add_attr('@author', $author) if $author;
$doc->add_attr('@updated', $entry->updated);
$doc->add_attr('@published', $entry->published) if $entry->published;
$doc->add_attr('@xml', $xml);
$doc->add_text($xml);
# データベースに文書を追加する
$c->model('Entries')->put_doc($doc);
return 1;
}
Hyper Estraierは属性に改行文字を格納できないため、
category要素やauthorの子要素は複数存在しうるので、
文書オブジェクトに属性と本文を追加し、
メンバの取得(Read)
sub get_entry :Atompub(read) {
my($self, $c) = @_;
# 検索条件オブジェクトを構築する
my $cond = Search::Estraier::Condition->new;
$cond->add_attr('@uri STREQ '.$c->req->uri); # URI が一致
# 検索を実行する
my $rs = $c->model('Search')->search($cond, 0);
# エントリが見つからなければ404エラーを返す
return $self->error($c, 404) unless $rs->doc_num;
# エントリリソースをセットする
my $doc = $rs->get_doc(0);
my $entry = XML::Atom::Entry->new(\$doc->attr('@xml'));
$self->entry_resource->body($entry);
return 1;
}
列挙と同じように検索オブジェクトを構築し、
メンバの削除(Delete)
sub delete_entry :Atompub(delete) {
my($self, $c) = @_;
$c->model('Search')->out_doc_by_uri($c->req->uri);
return 1;
}
URI を指定して文書を削除するメソッドを呼び、
代替表現への変換
代替表現
JSONへの変換にはGoogle::Data::JSONモジュールを使います。
use Google::Data::JSON qw(gdata);
sub end :ActionClass('RenderView') {
my($self, $c) = @_;
# altが指定されていなければ何もしない
unless ($c->req->param('alt')) {
}
# altがjsonであればJSON に変換する
elsif ($c->req->param('alt') eq 'json') {
$c->res->body( gdata($c->res->body)->as_json );
$c->res->content_type('application/json');
}
# altがjson-in-scriptであればコールバック関数をセットする
elsif ($c->req->param('alt') eq 'json-in-script' && $c->req->param('callback')) {
$c->res->body( $c->req->param('callback').'('.gdata($c->res->body)->as_json.')' );
$c->res->content_type('application/json');
}
}
クエリ変数に合わせて、
検索してみる
検索する前に、
適当なキーワードで検索してみましょう。たとえば、
GDataとの相違点
今回は簡単のために、
代替表現
GDataでは、
実際のGDataでは形態素解析を行って単語単位で検索していると思われます。一方、
GDataでは、
まとめ
Catalyst::Controller::AtomPubは、
しかし、
本連載に最後までお付き合いいただき、