Rails + Cicindelaでレコメンデーション付ウェブサイトの構築(2)

aike2009-01-25

目次

  1. ユーザ認証付Railsアプリの構築
  2. RailsアプリにCicindelaインタフェースを実装   ←いまここ
  3. Cicindelaの設定とバッチ処理設定

 
そんなわけで昨日作ったRuby on RailsアプリCicindelaとのインタフェース機能をつけてみます。
 

Cicindelaインタフェース用クラスの作成

CicindelaはWeb APIが用意されており、アプリケーション側の実装言語に依存せずに利用できるので非常に便利です。今回のアプリの場合アクセスするタイミングは3ヶ所で、以下に例を示します。
 
(a) ユーザID100の人がアイテムID500のウェブページをブックマークしたとき

http://localhost/cicindela/record?set=bookmark&op=insert_pick&user_id=100&item_id=500

(b) ユーザID100の人がアイテムID500のウェブページのブックマークを削除したとき

http://localhost/cicindela/record?set=bookmark&op=delete_pick&user_id=100&item_id=500

(c) アイテムID500のウェブページの関連ページを表示するとき

http://localhost/cicindela/recommend?set=bookmark&op=for_item&item_id=500

 
これらのラッパークラスCicindelaIfを作ってapp/controllers/application.rbに書きます。

require 'net/http'

class ApplicationController < ActionController::Base
  include AuthenticatedSystem

  helper :all
  protect_from_forgery

  class CicindelaIf
    @@host = 'localhost'
    @@service = 'bookmark'

    def self.set(user, item)  # (a)
      path = '/cicindela/record?set=' + @@service + '&op=insert_pick&user_id=' + user.to_s + '&item_id=' + item.to_s
      res = Net::HTTP::get(@@host, path)
    end

    def self.del(user, item)  # (b)
      path = '/cicindela/record?set=' + @@service + '&op=delete_pick&user_id=' + user.to_s + '&item_id=' + item.to_s
      res = Net::HTTP::get(@@host, path)
    end

    def self.get(item, max)   # (c)
      path = '/cicindela/recommend?set=' + @@service + '&op=for_item&item_id=' + item.to_s
      begin
        res = Net::HTTP::get(@@host, path)
      rescue             # レコメンデーションサーバが落ちている時は
        return []        # ガン無視して空配列を返す
      end
      unless /^(\d+\s+)+$/ =~ res  # かなり適当なエラー検知
        return []
      end
      ary = res.split(/\s+/).map{|w| w.to_i}
      ary.slice(0 .. max-1)
    end
  end
end
アプリケーションロジックへの組み込み

呼び出すタイミングは、(a)がBookmarksControllerクラスのcreate、(b)がdestroy、(c)がWebpagesControllerクラスのshowになります。
app/controllers/bookmarks_controller.rb(一部)

  def create
    @webpage = Webpage.find_by_url(params[:webpage][:url])
    if @webpage.nil?
      @webpage = Webpage.new(params[:webpage])
      unless @webpage.save
        render :action => "new"
      end
    end

    new_bm = false
    @bookmark = Bookmark.find_by_user_id_and_webpage_id(current_user.id, @webpage.id)
    if @bookmark.nil?
      @bookmark = Bookmark.new(:user_id => current_user.id, :webpage_id => @webpage.id)
      new_bm = true
    end
    @bookmark.comment = params[:bookmark][:comment]
    if @bookmark.save
      CicindelaIf::set(current_user.id, @webpage.id) if new_bm  # (a)
      flash[:notice] = 'Bookmark was successfully created.'
      redirect_to(bookmarks_url)
    else
      render :action => "new"
    end
  end

  def destroy
    @bookmark = Bookmark.find(params[:id])
    @bookmark.destroy
    CicindelaIf::del(current_user.id, @webpage.id)    # (b)
    redirect_to(bookmarks_url)
  end

app/controllers/webpages_controller.rb(一部)

  def show
    @webpage = Webpage.find(params[:id])
    @bookmarks = Bookmark.find_all_by_webpage_id(params[:id], :include => :user)
    # ランキングの上位5件取得
    ranking = CicindelaIf::get(params[:id], 5)    # (c)
    picks = Webpage.find(ranking)
    # ランキングの順番どおり並べ直して配列に格納
    @recommends = []
    ranking.each {|r|
      @recommends << picks.find{|w| w.id == r}
    }    
  end

せっかくCicindelaから関連度の高い順にidのリストが返ってきたのに、Rails側でWebpage.find()でレコード取得するときに、

select * from webpages where id in (id1, id2, id3, ... )

というようなSQLになってしまい順番が失われるので並べ直しています。(もっとスマートな方法はないのかな?)

ビュー側では受け取ったものをそのまま表示するだけ。
app/views/webpages/show.html.erb(一部)

<h3>◆おすすめページ</h3>
<% for rec in @recommends %>
  <%= link_to rec.title, rec.url %><br />
<% end %>
<br />

ね?簡単でしょ。

とはいえWeb APIが正しく反応してくれないと、これだけでは何も表示されません。そんなわけで次回はCicindela側の設定について書く予定です。