ライブドア製レコメンデーション・エンジン Cicindela の動作を確かめるために、簡単なAjaxアプリケーションを作ってみました。
前回の続きです。
JavaScript: Logger
Cicindela との連携部分を作る前に、簡単な Logger を作っておきます。ログの出力先には TEXTAREA 要素を想定しています。
var Logger = {
clear: function() {
$('#log').val("");
},
append: function(text) {
var dom = $('#log').get(0);
dom.value+= text + "\n";
dom.scrollTop = dom.scrollHeight;
}
};
Cicindela への入力
まずは入力部分から作っていきます。このアプリケーションにおいて必要な操作は「あるユーザがアイテムを選択した」、「あるアイテムをあるカテゴリに所属させる」の2つです。これらの操作を抽象化するために、Cicindela Web APIに対する Proxy となるオブジェクトを作ります。
var Cicindela = {
base: '/cicindela/',
set: 'sample',
_insert_op_url: function(op, item) {
return this.base +"record?set="+ this.set +"&item_id="+ item + "&op="+ op;
},
_send_req: function(url, cb) {
Logger.append("send Request to " + url);
$.ajax({ type: "GET",
url: url,
success: cb,
complete: function(xhr, status) {
Logger.append("receive Response["+ xhr.status +"] from "+ url);
}
});
},
set_category: function(item, category) {
this._send_req(this._insert_op_url("set_category", item) + "&category_id="+ category);
},
insert: function(user, item) {
this._send_req(this._insert_op_url("insert_pick", item) + "&user_id="+ user);
}
};
insert がアイテム選択、set_category がカテゴリ登録のためのメソッドです。実際にリクエスト送信を行うのは _send_req メソッドで、送信先URLと成功時に呼び出されるコールバック関数とを引数にとります。set プロパティには集計セットの名前を書いておいてください。
あとは「insert」ボタンが押されたときに、これらの API を呼び出します。
// $(document).ready において
$('button#insert').click(
function(e) {
var user_id = $('#username').val();
if(user_id == "") {
return false;
}
$.each(data, function(category_name, category_data) {
var category_id = category_data.id;
var container = $("#"+ category_name);
var values = container.val();
$.each(values,
function(i, v) {
Cicindela.set_category(v, category_id);
Cicindela.insert(user_id, v);
$('#username').val("");
});
});
return false;
});
このアプリケーションの場合、カテゴリ登録はデータの初期化時にまとめて行った方が効率的なのですが、「同じアイテムを、何度同じカテゴリに登録しても問題ない」ことを確かめるために、あえてアイテム選択時に毎回登録を実行しています。
これで Cicindela への入力準備が整いました。ユーザ名を入力して、適当にアイテムを選択して「insert」ボタンを押してみます。成功すれば次のようなログが出力されるはずです。
send Request to /cicindela/record?set=sample&item_id=Ruby&op=set_category&category_id=1
send Request to /cicindela/record?set=sample&item_id=Ruby&op=insert_pick&user_id=cicindela
receive Response[204] from /cicindela/record?set=sample&item_id=Ruby&op=insert_pick&user_id=cicindela
receive Response[204] from /cicindela/record?set=sample&item_id=Ruby&op=set_category&category_id=1
またMySQLサーバにログインして、categories テーブルや picks テーブルにデータが入っていることを確認します。
Cicindela からの出力
続いて Cicindela からレコメンデーション・データを取り出してみます。必要なのは「特定アイテムに対するレコメンデーション(関連アイテム)取得 (=item to item)」なので、次のような get_for_item メソッドを Cicindela オブジェクトに追加します。
var Cicindela = {
/* ...略... */
get_for_item: function(item, category, cb) {
var url = this.base +'recommend?set='+ this.set
+'&op=for_item&item_id='+ item +"&category_id="+ category;
this._send_req(url, cb);
}
};
そして「get」ボタンが押されたときにこのメソッドを呼び出します。第3引数として渡すコールバック関数は、正常にレスポンスが受信できたときに受信データを引数として呼び出されます。Cicindela からのレスポンスは改行区切りの文字列データです。ここでは単に出力用の要素に html メソッドで書き込んでおきます。
// $(document).ready において
$('#get').click(
function(){
var item_id = $('#items').val();
$.each(data, function(category_name, category_data) {
Cicindela.get_for_item(item_id,
category_data.id,
function(data) {
$("#output-"+ category_name).html(data)
});
});
});
以上で一通りの機能が完成しました。ユーザ名と選択アイテムを変えて何度か入力を行い、レコメンデーションを取得してみましょう。その際には自分なりに”シナリオ”を決めて、そのシナリオに沿った結果になるか試してみると面白いです。例えば:「Perl好きはEmacsやVimを使う」、「WindowsユーザにもMacユーザにもLinuxユーザにもJava好きはいる」、「EmacsとVimを両方ともよく使うユーザは少ない」などなど。
完全なソースを gist に置いておきます。
ライブドア製レコメンデーション・エンジン Cicindela の動作を確かめるために、簡単なAjaxアプリケーションを作ってみました。スクリーンショット:
「好きなプログラミング言語」と「よく使うソフトウェア」についての情報を収集し、その結果に基づいておすすめの言語・ソフトを算出する、一種のアンケートです。
現実にはAjaxアプリケーションから直接 Cicindela のWeb APIを叩くのは難しいと思いますが(いたずらされ放題になってしまうので)、機能をテストするのには手軽です。以下に作り方を紹介しておきます。
- Cicindela はセットアップ済みとします(CentOS 5にセットアップする手順はこちら)
- カテゴリー機能を使います
- アイテムid・ユーザidは文字列です
- jQuery 1.3.2 を使います
- Firefox 3で動作確認します(ほかのブラウザでも動くと思いますが)
データベースの初期化
create_init_sql.pl を実行して使用するデータベースを作成します。DB名は sample としました。
perl create_init_sql.pl --db_name=sample | mysql -uroot
cicindelaの設定
lib/Cicindela/Config/_common.pm に集計セットを定義します。
'sample' => {
datasource => [ 'dbi:mysql:sample;host=localhost', 'cicindela', 'cicindela' ],
filters => [
'PicksExtractor',
'InverseUserFrequency',
'ItemSimilarities',
],
recommender => 'ItemSimilarities::LimitCategory',
refresh_interval => 1,
use_user_char_id => 1,
use_item_char_id => 1,
discard_user_id_char2int => '6 month',
},
カテゴリ機能を使いたいので、フィルタとして ItemSimilarities を、レコメンダとして ItemSimilarities::LimitCategory を指定します。またユーザidとアイテムidに文字列を使用したいので、 use_*_char_id を設定します。結果を確認しやすくするために refresh_interval を 1 にしていますが、実運用時にはもっと大きな値を指定してください。
HTML
HTMLのbody内を抜粋すると次のようになります。SELECT 要素は後からJavaScriptで初期化するので、この段階では空っぽです。
<h2>追加</h2>
<div class="insert">
<p>username: <input type="text" id="username" /></p>
<fieldset><legend>好きな言語</legend>
<select multiple="multiple" id="langs"></select>
</fieldset>
<fieldset><legend>よく使うソフト</legend>
<select multiple="multiple" id="softs"></select>
</fieldset>
<p class="submit-space"><button id="insert">insert</button></p>
</div>
<h2>取得</h2>
<div class="recommend">
<p><select id="items"></select></p>
<fieldset><legend>おすすめ言語</legend>
<div id="output-langs"></div>
</fieldset>
<fieldset><legend>おすすめソフト</legend>
<div id="output-softs"></div>
</fieldset>
<p class="submit-space"><button id="get">get</button></p>
</div>
<h2>ログ</h2>
<textarea id="log"></textarea>
JavaScript: データの定義
使用するデータは JavaScript のオブジェクトとして、次のような構造で定義します。langs と softs という2つのカテゴリがあり、それぞれに数値のid(1,2)が割り当てられています。
var data = {
langs: { id: 1,
list: ['Perl', 'PHP', 'Ruby', 'Python', 'Java',
'JavaScript', 'CSharp', 'Erlang', 'CommonLisp'] },
softs: { id: 2,
list: ['Windows', 'Linux', 'Mac', 'Emacs', 'Vim',
'Eclipse', 'NetBeans', 'VisualStudio', 'Dreamweaver'] }
};
JavaScript: SELECTの初期化
document の ready イベントにてSELECT要素を初期化します。Cicindela に渡す item_id としてアイテムの名前をそのまま使うので、OPTION要素の value 値にはアイテム名を設定しておきます。
$(function() {
$.each(data, function(category_name, category_data) {
var all_items = $('#items');
var category_items = $("#"+ category_name);
$.each(category_data.list,
function(i,l) {
$('<option />').attr('value', l).text(l)
.clone().appendTo(category_items)
.clone().appendTo(all_items);
});
});
});
続く。
全てのブラウザで安定してSELECT要素を操作するためのプラグイン……というか、実質IE対策プラグインです。
gits: 256077(動作テスト)
ごく小さなプラグインで、余計な機能は一切ついていません。使用できるメソッドは clearOptions と addOption の2つです。当たり前ですが、SELECT要素以外に使っても意味はありません。
- clearOptions()
- 全てのOPTION要素を削除します
addOption(text, value, selected)- 新しいOPTION要素を追加します。引数はそれぞれ表示されるテキスト、値、選択状態です
こんな感じで使います。
//start月からend月までの選択肢を作る
function updateMonth(start, end) {
var select = $('#month');
var current = select.val();
select.clearOptions();
for(var i=start; i<=end; ++i) {
select.addOption(i + "月", i, i == current)
}
}
全てのブラウザで(というかIEで)安定した結果を得るために、次の点に注意すると良いでしょう:
- 構築直後に val メソッドによる選択は行わない。代わりに addOption メソッドの第3引数でデフォルトの選択状態を設定する
- 構築処理全体を setTimeout で遅延実行する
検索で来る方のために、このプラグインを作るまでにIEで遭遇した問題も書いておきます:
- 構築後、SELECTの横幅がおかしくなる。構築を繰り返すたびにだんだんと短くなり、最後には消滅する
- 構築に時間がかかる。そのために処理の途中でSELECTの表示がチラつく
- 構築直後に val メソッドで選択状態を変更すると「selected プロパティを設定できませんでした。未定義のエラーです。」となる(val メソッドを使わず、addOption メソッドの第3引数を使うことで回避可能)
- ページ読み込み直後にSELECTを構築すると、選択がレンダリング結果に反映されない(構築処理全体を setTimeout で遅延実行することで回避可能)
コード全体:
Prototype.js 風の Class を書きたいが、ライブラリ全体は読み込みたくない、というときのために、必要最低限のコードを抜き出してみた。要jQuery。
var Class = (function() {
function subclass() {}
return {
create: function(parent) {
function klass() {
this.initialize.apply(this, arguments);
}
var index = 0;
if(jQuery.isFunction(parent)) {
index = 1;
subclass.prototype = parent.prototype;
klass.prototype = new subclass;
}
for(; index < arguments.length; ++index) {
jQuery.extend(klass.prototype, arguments[index]);
}
return klass;
}
}
})();
最大の違いは $super による親クラスのメソッド呼び出しができないこと。Prototype.jsのチュートリアルの例は次のようになる。
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
say: function(message) {
return this.name + ': ' + message;
}
});
var Pirate = Class.create(Person, {
say: function(message) {
//親クラスのprototypeに直接アクセス
return Person.prototype.say.call(this, message) + ', yarr!';
}
});
var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John: ahoy matey, yarr!"
mix-in の例などはそのまま動く。
|