Displaying posts tagged with

“cicindela”

CakePHP用Cicindelaライブラリ

CakePHP から レコメンデーションエンジン Cicindela にアクセスするためのライブラリを公開しました。

http://github.com/tkyk/cakephp-cicindela

使い方はREADMEを読んでください。Cicindela本体のWeb APIとほとんど同じ構造なので、特に迷うことはないだろうと思います。

ちなみに Configure::read(‘debug’) > 1 で実行すると、DboSourceと同じようにリクエスト情報が一覧表示されます。デバッグに便利です。こんな感じ:

(cicindela) 2 request(s)
NrURLStatusTook (ms)
1http://host/cicindela/recommend?set=pick&op=for_item&item_id=RubyHTTP/1.1 200 OK2887
2http://host/cicindela/recommend?set=pick&op=for_item&item_id=PHPHTTP/1.1 200 OK1346

….
以下、このライブラリを作りながら考えたこと。

このライブラリはDataSource + Behaviorという2クラス構成になっている。最初はDataSourceだけでできるかと思っていたのだが、無理だった。現在のCakeのModelクラスは、DataSourceとDboSourceの関係で言うなら”DboModel”であって、非DboのDataSourceを扱うには適していない。

現在のModelが”DboModel”であるように、XXXSourceに対してはXXXModelが存在することが望ましい。CakePHP 1.3からはDataSourceがプラグイン化できるようなので、同時にModelのベースクラスを提供するようにすれば、ある程度は改善されると思う。ただそれでも結局(プラグインの)ユーザがいちいちクラスの継承関係に気を使わなければいけない点が煩わしい。理想を言うなら、1.非常に簡素なBaseModelクラスを用意する、2.DataSourceにモデル初期化用コールバックを設ける、3.初期化用コールバックにおいてBehaviorをattachする、DataSource固有の機能はBehaviorに実装する……という構成にできれば良いのではないか。

Cicindelaを使ったAjaxアプリケーションサンプル 2

ライブドア製レコメンデーション・エンジン 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アプリケーションサンプル

ライブドア製レコメンデーション・エンジン Cicindela の動作を確かめるために、簡単なAjaxアプリケーションを作ってみました。スクリーンショット:

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);
                        });
             });
  });

続く。

CentOS 5にcicindelaをインストール

ライブドア製のレコメンデーションエンジン cicindela を CentOS 5.4 にインストールした記録です。極力 yum/rpm によるインストールを行うようにしています。参考にさせていただいたサイトははてブのcicindelaタグにまとめておきます。

cicindela ソースコードのチェックアウト

Google Code でホストされている cicindela 本体のソースコードを /home/cicindela にチェックアウト。ここは公式の手順と全く同じなので、そちらを参照のこと。

Perlモジュールのインストール

DBI/DBD-mysql は mysql-server を入れたら付いてくるので、その他のモジュールをrpmforgeから導入。公式サイトには書いていないが、私の環境では Class::Data::Inheritable と DBIx::ContextualFetch も必要だった。

sudo yum --enablerepo=rpmforge install \
  perl-Ima-DBI \
  perl-Time-Piece \
  perl-Log-Log4perl \
  perl-Module-Pluggable \
  perl-Class-Singleton \
  perl-Class-Data-Inheritable \
  perl-DBIx-ContextualFetch

ここで次のようなエラーが出た。

/usr/share/man/man3/Sys::Syslog.3pm.gz from install of perl-Sys-Syslog-0.27-1.el5.rf.i386 conflicts with file from package perl-5.8.8-27.el5.i386

Log-Log4perl の依存パッケージとしてインストールされる perl-Sys-Syslog パッケージが、CentOS 5(RHEL5)標準の perl パッケージに含まれる Sys::Syslog のファイルと衝突するらしい。参考URL。 仕方がないので Log::Log4perl だけは cpan コマンドでインストールした。

sudo cpan -i Log::Log4perl

MySQL のインストールと設定

MySQL をインストール。

yum install mysql-server

公式のインストールガイドには /home/cicindela/etc/mysql/my.cnf を /etc/my.cnf にコピーせよとあるが、この中にはかなり癖の強い設定が含まれているので、既存のサーバを流用する場合は要注意。というか、実質的には cicindela 専用サーバが必要になると考えた方が良いだろう。

特に注目すべき設置項目は以下の4つ。

max_heap_table_size = 1024M
MEMORY テーブルの最大サイズ。cicindela は MEMORY テーブルに入力データを保存するため、それなりに大きなサイズが必要になる。実際のところ、どのくらいあれば十分と言えるのだろうか…。
innodb_log_file_size = 256M
この設定値を後から変更すると起動不能に陥る場合があるので注意。変更する場合はこちらのサイトを参考に。
innodb_lock_wait_timeout = 1800
デッドロックと判断するためのタイムアウト。デフォルトは50秒なので、極端に長い。おそらくはバッチ処理の都合でこうなっているのだろう。
innodb_flush_log_at_trx_commit = 2
この設定値がどの程度意味を持つものなのか、正直言って分からない。MySQLリファレンスによれば、値を2にすることで、『より良い性能』と引き換えに『OS のクラッシュか停電によって、最後の秒のトランザクションが消されてしまいます。 』とのこと。

cicindela 用データベースの作成

misc/create_init_sql.pl を実行して、DB初期化用SQLを生成する。このスクリプトで生成できるのは CREATE DATABASE文、CREATE TABLE文 さらに –grant_* オプションを指定することで GRANT文も生成できる。

cd misc
perl create_init_sql.pl --db_name=cicindela \
  --grant_user=cicindela \
  --grant_host=localhost \
  --grant_pass=cicindela_pass | mysql -uroot

cicindela の設定

cicindela の設定ファイル lib/cicindela/lib/Cicindela/Config/_common.pm の中で最小限の設定を含む集計セットを定義する。ここでは pick という名前にした。datasource に指定するDB名やhost名は適宜変更のこと。

$C{SETTINGS} = {
    'pick' => {
        datasource =>  [ 'dbi:mysql:cicindela;host=localhost', 'cicindela', 'cicindela_pass' ],
        filters => [
            'PicksExtractor',
            'InverseUserFrequency',
            'ItemSimilarities',
        ],
        recommender => 'ItemSimilarities',
        refresh_interval => 1,
        },
};

設定を変更したらApacheを再起動。error_log や logs/log.txt にエラーが出ていないことを確認しておく。

動作確認

以下のようなURLにアクセスして、HTTPステータスコード204が返ることを確認。user_id や item_id を変えつつ何度かアクセスしておく。

http:///cicindela/record?set=pick&op=insert_pick&user_id=3&item_id=23

入力バッファテーブルにデータが入っていることを確認。

mysql> select * from picks_buffer;
+----+---------+--------------+---------+--------------+-----------+---------------------+
| id | user_id | user_char_id | item_id | item_char_id | is_delete | timestamp           |
+----+---------+--------------+---------+--------------+-----------+---------------------+
|  1 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:03 |
|  2 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:11 |
|  3 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:29 |
|  4 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:52 |
|  5 |       5 | NULL         |      23 | NULL         |         0 | 2009-12-28 13:24:00 |
|  6 |       5 | NULL         |      29 | NULL         |         0 | 2009-12-28 13:24:20 |
+----+---------+--------------+---------+--------------+-----------+---------------------+
6 rows in set (0.01 sec)

手動で bin/flush_buffer.pl を実行。

./bin/flush_buffer.pl

入力バッファがクリアされ、一時データテーブルに入力されることを確認。

mysql> select * from picks_buffer;
Empty set (0.00 sec)

mysql> select * from picks;
+---------+---------+---------------------+
| user_id | item_id | timestamp           |
+---------+---------+---------------------+
|       3 |      23 | 2009-12-25 17:28:03 |
|       5 |      23 | 2009-12-28 13:24:00 |
|       5 |      29 | 2009-12-28 13:24:20 |
+---------+---------+---------------------+
3 rows in set (0.00 sec)

次に bin/batch.pl を実行、集計結果を算出する。初回実行時には2回実行する必要がある。ある程度以上のデータが溜まっていないと算出されないので注意。

./bin/batch.pl
mysql> select * from item_similarities_online;
+----------+----------+------------------+
| item_id1 | item_id2 | score            |
+----------+----------+------------------+
|       23 |       25 | 1.58496250072116 |
|       25 |       23 |                0 |
+----------+----------+------------------+
2 rows in set (0.00 sec)

最後に以下のようなURLにアクセスして、Web API経由でリコメンデーション・データを取得できることを確認。


http:///cicindela/recommend?set=pick&op=for_item&item_id=23

以上で cicindela の最低限の動作は確認できた。

daemontools のインストール

次に flush_buffer.p と batch.pl の実行を自動化するため、daemontools をセットアップする。まず daemontools-toaster のSRPMを http://www.qmailtoaster.org/ から取得して、ビルド&インストール。

rpmbuild --rebuild daemontools-toaster-0.76-1.3.6.src.rpm
rpm -Uvh daemontools-toaster-0.76-1.3.6.$ARCH.rpm

cicindela のバッチスクリプトに対するシンボリック・リンク作成。

sudo ln -s /home/cicindela/etc/service/cicindela_batch /service/
sudo ln -s /home/cicindela/etc/service/cicindela_flush_buffers /service/

/etc/inittab の末尾に svscan の起動コードを追加。

SV:123456:respawn:/command/svscanboot

init に HUP シグナルを送ってinittabを読み込み直し、svscanを起動する。

sudo kill -HUP 1

ps コマンド等で svscan, supervise プロセスが起動していることを確認しておく。

この状態でもう一度データの入力テストを行い、自動的に一次データ・計算結果が更新されることを確認。

このまま放っておくとどんどんログが肥大化していくので、ログローテーション用設定ファイル /etc/logrotate.d/cicindela を以下の内容で作成する。

/home/cicindela/var/logs/log.txt {
  daily
  create 0666 root root
  rotate 2
}

次はより実践的な構成でテストを行う予定。