ドワンゴの荒木です。
2010/7/16に行われる、ドワンゴ技術勉強会の生放送を行います。

番組URL

ドワンゴ技術勉強会

日時

7/18 20:00~23:00

  • 20:10 - : 株式会社ライブドア 伊勢 幸一様
  • 20:50 - : クックパッド株式会社 佐々木 達也様
  • 21:30 - : ドワンゴ社員3名

詳細

株式会社ライブドア 伊勢 幸一様

タイトル:IaaSシステム構築のポイント

仮想マシンや仮想ネットワークなどの技術的概要とライブドアのクラウドっぽいホスティングサービスを例に、IaaSシステムの構築上、注意しなければならないポイント等を紹介します

クックパッド株式会社 佐々木 達也様

タイトル:クックパッドでのHadoop利用

たべみるというサービスのデータ解析でHadoopを用いた事例について紹介します

株式会社ドワンゴ 研究開発本部 山陰 祐司

タイトル:とある技術の超分散法(スケールアウト)

大量のデータを解析するためにいかにスケールアウトという技術を用いるかについて実例を交えながら論じる

株式会社ドワンゴ 研究開発本部 阿部 克宏

タイトル:Hadoop, HBase の 環境構築、運用を行ってみて

大容量データの解析基盤を構築し運用した際の事例をトラブルを交えながら紹介する

株式会社ドワンゴ 研究開発本部 小野 侑一

タイトル:Web開発における分散データベースの利用

既存のリレーショナルデータベースの持つ課題を挙げ、分散データベースがそれらの課題にどのように対応するか、分散データベースの一つであるCassandraを例に挙げて紹介する

今後も、ドワンゴで行われる技術勉強会の生放送を行っていきます。よろしくおねがいします。

こんにちは!なんだかよくわからないけど、いつの間にか航空宇宙課を作ることになっていたので、せっかくだしエンジニアの僕らも宇宙工学について勉強しようと思い立ち、勉強会を開くことになりました。

あとせっかくなので、NKHの方にお願いして、生放送もしてもらえることになりました! ページはこちら:【ネットに生まれて】ドワンゴ航空宇宙課設立記念勉強会【宇宙で遊ぶ】

日時

6/18 16:00〜22:00

  • 16:15 - : BeDaiさん
  • 17:00 - : 尻Pさん
  • 18:30 - 19:30 : 休憩
  • 19:30 - : 堀江貴文さん
  • 21:00 - : ドワンゴ社員二名

詳細

BeDaiさん : 「宇宙(そら)への階段 〜 アニメとは違う宇宙産業の初歩 〜」

僕らエンジニア、理系とはいえ分野のまったく異なる宇宙の話は専門外です。 そんな僕らに、正しい宇宙の基礎知識からお話して頂きます。僕らが思い浮かべる宇宙って、実は違うかも?学校で勉強したはずだけど、なんとなくフィクションやSFで上書きされてしまった宇宙の知識。まずは最初の入り口から、お話して頂きます。

尻Pさん : 「はやぶさ迎撃報告/草の根宇宙開発」

カプセルを持ち帰って、日本の話題独り占めの工学実験機はやぶさ。先日の生放送でもお送りしましたが、今度は現地取材での写真も交えて、熱く語って頂きます。 そして、僕らが宇宙で遊ぶためには、どうしたらいいのか?ロケット?衛星?個人じゃ到底関われなさそうな宇宙に迫るには、どうすれば何が出来るのか、その方法と可能性をこっそり教えてもらいます。

堀江貴文さん(SNS株式会社) : 「民間宇宙開発の現状」

民間の宇宙開発会社SNS株式会社のファウンダー、そして現在民間で最も宇宙に近い(と思う)堀江さんに、開発中のロケットエンジンの話、その後の野望、宇宙をビジネスの場として見た堀江さんはどこへ向かうのかを語って頂きます。

ドワンゴ社員二名

せっかく素敵なお話を聞けるので、弊社のエンジニアにも無理を言って勉強してもらいました。 宇宙に出てもインターネットがなくちゃ生きていけない僕たちが、宇宙でネットにつながるために必要な周辺技術ってなあに?と、宇宙でネットにつながっても、いつも通りにPCをいじれるの?実はそうじゃないらしい!半導体のソフトエラー対策の最新技術を、ちらっとお話します。

関連 「ドワンゴ航空宇宙課」作ります!尻P、ホリエモンら登壇の設立記念勉強会を生放送

はじめに

この記事では、JavaScript 用のテンプレートエンジン Jarty を紹介します。

Jarty (じゃーてぃー) とは、PHP の有名なテンプレートエンジンである Smarty を JavaScript に移植したものです。ただし、完全に再現しているわけではなく、いくつかの機能は制限されています。

Jarty には以下のような機能があります。書式の多くは Smarty 互換です。

  • 変数の置換 {$foo} => "abc"
  • {if} {else} {/if} などの条件分岐
  • {foreach} {/foreach} などのループ
  • {$foo|upper|escape} などのフィルタ (パイプ)
  • etc.

ソースコードは The MIT License として公開しています。

GitHub 上のリポジトリ: http://github.com/kotas/jarty

また、弊社ウェブサービスであるニコニコ動画の一部ページでも利用しています。

背景

Smarty は主にサーバーサイドで使うものですが、Jarty はクライアントサイドで利用する事を想定しています。

ウェブサービスによって JSONP 形式で公開されている API を JavaScript から利用する場合、結果を JavaScript 内のオブジェクトとして受け取る事が出来ますが、その表示をどうするか困る場合があります。

例えば、以下のような方法で表示できます。

  1. HTML 文を組み立て、innerHTML にまるごと入れる
  2. class や id などに基づいて動的に DOM を書き換える

1 の方法では、JavaScript を書く人と HTML を書く人が違う場合に困ります。また、JavaScript のコード中に HTML が混じるため、読みづらくなる欠点があります。

2 の方法では、JavaScript 中に HTML が混じる事はありませんが、最終的な DOM がどうなるのか JavaScript コードを追わないとわからない、DOM 構造を変えたら JavaScript 側の修正も必要、などの欠点があります。

Jarty を使うと、テンプレートの記述を分離でき、かつ比較的自由度の高いデザインが可能になります。また、テンプレートは HTML ベースなので、JavaScript が分からない人でも読み書きできます。

使い方

jarty.js をページ上に読み込みます。

<script src="jarty.js" type="text/javascript" charset="utf-8"></script>

簡単な使い方は以下のようになります。

var template = "Hello, {$foo}!";
var dictionary = { foo: "world" };

Jarty.eval(template, dictionary);
    // => "Hello, world!"

もっと複雑な例は、以下のページから確認できます。

内部の処理について

Jarty は、受け取ったテンプレート文字列を内部的に JavaScript の関数オブジェクト (Function) にコンパイルしています。

例として、以下のようなテンプレートは

Hello, {$foo}!

以下のような JavaScript 関数にコンパイルされます。(実際にはもうちょっと複雑です)

function (d) {
    var s = "";
    s += "Hello, ";
    s += d["foo"];
    s += "!";
    return s;
}

Jarty.compile() 関数で、テンプレートをコンパイルして関数オブジェクトを取得できます。

var compiled = Jarty.compile(template);
compiled({ foo: "world" });
    // => "Hello, world!"
compiled({ foo: "Jarty" });
    // => "Hello, Jarty!"

同じテンプレートを何度も利用する場合は、コンパイルされた関数オブジェクトを 使い回す事で速度を得る事ができます。

ベンチマーク

ベンチマーク結果は JavaScript エンジンの性能によって大きく左右されるため、Windows マシン上 (Intel Core2 1.86GHz + 2GB RAM) でいくつかのブラウザで確認してみました。

ベンチマークに利用したページ

いくつかの変数置換、{foreach}、{if} を含むテンプレート文字列を 1000 回評価すると、以下のようになりました。

Each-time Compile
  Mozilla Firefox
    1224 ms (817 回/秒)
  Internet Explorer 6
    3828 ms (261 回/秒)
  Internet Explorer 7
    3984 ms (251 回/秒)
  Internet Explorer 8
    2500 ms (400 回/秒)
  Google Chrome
    381 ms  (2625 回/秒)
  Opera
    1343 ms (745 回/秒)
  Safari
    703 ms  (1422 回/秒)

Google Chrome の早さが目立ちますね。一番遅い IE6〜7 でも 250 回/秒ほど回りました。

また、コンパイル済みの関数オブジェクトを使い回して 1000 回評価すると、以下のようになります。

Precompile
  Mozilla Firefox
    65 ms  (15385 回/秒)
  Internet Explorer 6
    265 ms (3774 回/秒)
  Internet Explorer 7
    281 ms (3559 回/秒)
  Internet Explorer 8
    156 ms (6410 回/秒)
  Google Chrome
    7 ms   (142857 回/秒)
  Opera
    31 ms  (32258 回/秒)
  Safari
    14 ms  (71429 回/秒)

毎回コンパイルするのに比べて、コンパイル済み関数オブジェクトを使い回すと、圧倒的に速度が勝っているのを確認できます。

おわりに

JavaScript 用のテンプレートエンジン Jarty を紹介しました。

なお、Jarty のソースコード、及びこの記事中に含まれるコードは全て The MIT License です。

Jarty のソースコードは GitHub 上のリポジトリ を参照してください。

Enjoy!

2010年3月10に行われた、情報処理学会創立50周年記念全国大会内
招待講演「CGMの現在と未来:初音ミク、ニコニコ動画、ピアプロの切り拓いた世界」内で発表したスライドです。

はじめに

この記事では、MySQL を使って簡単なメッセージキューを手軽に実装する方法を解説します。

メッセージキューとは、メッセージを一時的に溜めておき、順次処理するための仕組みです。迅速なレスポンスが必要な Web アプリケーションにおいて、時間のかかる処理を非同期に行うために、バックグラウンドで順次処理していくような場合に利用できます。

簡単なメッセージキューと言っても、大規模な運用にも耐えられる程度の速度と堅牢性を持ちます。

また、ここで解説している方法で作られたメッセージキューは、弊社ウェブサービスであるニコニコ動画に最近追加されたtwitter連携機能でも利用しています。

メッセージキューを作るにあたって

今回実装するメッセージキューは

  • メッセージの追加(push)を高速に行う事ができる
  • メッセージの取得(pop)はある程度高速に行う事ができる
  • 多くのクライアントから同時に push/pop を行っても矛盾が発生しない
  • MySQL に特別な拡張を必要としない

事を目指します。

Q4M について

MySQL 上で動作するキューエンジンとしては、サイボウズ・ラボ様によって開発・保守されている Q4M が有名です。MySQL5.1 系で動作するプラガブルストレージエンジンとして実装されていて、SQL を利用してキューの操作を行う事ができます。

残念ながらニコニコ動画では MySQL 5.0 系を利用しており、Q4M の利用は諦めざるを得ませんでしたが、MySQL 自体は既に何年も運用していてノウハウがあり、それを活かしたいので、特に拡張機能を使わずに独自のメッセージキューを実装する事にしました。

InnoDB を利用

ニコニコ動画では、基本的に MySQL 組み込みのストレージエンジンである InnoDB を利用しています。

InnoDB は高度なメモリキャッシュ機構と ACID を満たすトランザクション機構を備え、パフォーマンスに優れたストレージエンジンです。

今回実装するメッセージキューは、MySQL の基本機能を利用して作るため、ストレージエンジンがある程度自由に選ぶ事ができます。今回は InnoDB を利用します。

実装方法

メッセージキューテーブルを作成

メッセージキューが保存されるテーブルとして、以下の test_queue テーブルを作成します。

CREATE TABLE test_queue (
  `id` BIGINT UNSIGNED NOT NULL
        AUTO_INCREMENT,
  `locked_until` TIMESTAMP NOT NULL
        DEFAULT "0000-00-00 00:00:00",
  `data` BLOB NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB;

テーブル中のカラムはそれぞれ、

  • id: プライマリキーとなる数値ID
  • locked_until: ロックが無効となる日時
  • data: メッセージ内容

を表します。

基本的にメッセージの内容は data カラムに収まる事を想定していますが、例えば優先度付きキューを作るためにカラムを追加する事も可能です。

メッセージの追加

メッセージキューの末尾にメッセージを追加するには、単に INSERT をするだけです。

INSERT INTO test_queue SET data = "message body";

メッセージ内容である data カラムのみ指定します。単なる INSERT 操作であるため、高速です。

また、矛盾が発生しない事はストレージエンジンによって保証されます。

メッセージの取得

メッセージキューの先頭にあるメッセージを取得するには、以下の手順をたどります。

  1. 先頭のメッセージをロックする
  2. ロックしたメッセージのIDを取得する
  3. ロックしたメッセージの内容を取得する
  4. ロックしたメッセージを削除する

一つ一つ解説します。

1. 先頭のメッセージをロックする

メッセージを処理するクライアントが複数存在する場合、ロックを行わないと1つのメッセージを2つのクライアントが同時に取得してしまう事が起こりえます。

そこで、今から処理を始めるメッセージをロックする事で、他のクライアントからは利用できないようにします。

以下の SQL を実行します。

UPDATE test_queue
  SET id = LAST_INSERT_ID(id),
    locked_until = NOW() + INTERVAL 10 SECOND
  WHERE locked_until < NOW() ORDER BY id LIMIT 1;

なぜこの SQL が先頭のメッセージをロックする事になるのか考えてみましょう。

まず

id = LAST_INSERT_ID(id)

という部分についてですが、これはロックしたメッセージのIDを取得するためのものです。後で解説します。

locked_until = NOW() + INTERVAL 10 SECOND

は、locked_until カラムに現在時刻から10秒後の時刻を設定しています。

WHERE locked_until < NOW()

は、locked_until カラムが現在時刻より前のものを取得する事を表しています。

ORDER BY id LIMIT 1

で、id 昇順 (つまり追加された順) に並べ、先頭1件を取得しています。

locked_until カラムに10秒後の時刻を設定すると、他のクライアントは10秒間、WHERE 条件によりこのメッセージを SELECT しなくなります。このようにして、ロック機構を実現しています。

仮に、クライアントが突然終了しても、10秒後にはロックが解除され、他のクライアントによって処理される事になります。

2. ロックしたメッセージのIDを取得する

以下の SQL を実行します。

SELECT LAST_INSERT_ID();

LAST_INSERT_ID 関数 は、引数付きで呼び出された場合、内部に引数を保存した上で、引数をそのまま返します。引数なしで呼び出された場合、最後に保存された内部の引数を返します。

メッセージをロックした際に id = LAST_INSERT_ID(id) という指定を行っていましたが、そのお陰でメッセージのIDを取得する事ができます。

また、MySQL のクライアントライブラリによっては、この取得を関数で行う事ができます。

例として PHP では mysql_insert_id 関数PDO::lastInsertId メソッド を利用する事で取得できます。

3. ロックしたメッセージの内容を取得する

得られたメッセージのIDを使って、普通に SELECT します。

SELECT data FROM test_queue WHERE id = ?;

4. ロックしたメッセージを削除する

得られたメッセージのIDを使って、普通に DELETE します。

DELETE FROM test_queue WHERE id = ?;

通常は、この時点でレコードが削除されるため、他のクライアントからは参照できないままですが、仮にレコードが削除される前にプロセスが終了してしまっても、10秒後には新たなクライアントが処理を開始します。

以上で、メッセージキューを実装できました。

PHP での実装例

上記を元に PHP でメッセージキューを実装した例を以下で公開しています。

http://github.com/kotas/myqueue

メッセージキューの実装である MyQueue クラスのURLは以下です。

http://github.com/kotas/myqueue/blob/master/MyQueue.php

ベンチマーク

ニコニコ動画で使用している典型的な MySQL サーバー上で 1000 個のメッセージを追加(push)/取得(pop)した場合、以下のようになりました。

push
    Total: 0.162686 sec.
    QPS:   6146.815289 query/sec.
    SPQ:   0.000163 sec/query.
pop
    Total: 1.719178 sec.
    QPS:   581.673348 query/sec.
    SPQ:   0.001719 sec/query.

追加(push) は単なる INSERT なので高速ですが、取得は UPDATE + SELECT + DELETE という3つのクエリが含まれるため、やや低速になります(581メッセージ/秒)。

なお、ベンチマークのコードは以下にあります。

http://github.com/kotas/myqueue/blob/master/benchmark.php

おわりに

MySQL を使ったお手軽なメッセージキューの実装方法を紹介しました。

なお、この記事中に含まれるコードは全て The MIT License です。

Enjoy!

このエントリでは Ruby on Rails と MySQL を使って日本語の全文検索を行う方法を記述する。Ruby on Rails のバージョンは 2.0.2、MySQL のバージョンは 5.0.67、Tritonn のバージョンは 1.0.12、Hyper Estraier のバージョンは 1.4.10 を使用した。サンプルの文章データとして、あらゆる日本人にとって極めて身近な著作権切れ文章である『ドグラ・マグラ』と『黒死館殺人事件』を利用した。処理のために整形したデータは本エントリに添付しておく。またデータベースへアクセスするコードではマイグレーションを除きできるだけベンチマークを取るようにし、その結果は本エントリの最後に記載する。

ページネーション

Rails でページネーションを実現する will_paginate という plugin は ActiveRecord に標準でついているような検索メソッドには対応しているものの、全文検索用 plugin による検索のように特殊なメソッドを使う検索結果のページネーションには対応していない。しかし will_paginate/lib/will_paginate/finder.rb を見ると、手軽に独自のやり方を追加できることがわかる。acts_as_tritonn の find_fulltext に対応したページネーションを実装すればよい。

まずは will_paginate をインストールする。公式の Wiki には「The gem is preferred method of installation; if you have the plugin in "vendor/plugins/will_paginate/" directory, it may be best to remove it and simply configure your application to load the gem.」とあるので次のようにして gem からインストールする:

gem sources -a http://gems.github.com
gem install mislav-will_paginate

今後は will_paginate をインストールしているものとして話を進める。全文検索におけるページネーションへの対応は各 plugin の項で説明する。

全文検索を行うことのできる二つの plugin

MySQL で日本語の全文検索を行うには二つのメジャーな方法がある、Tritonn を使う場合と Hyper Estraier を使う場合だ。どちらの場合も SQL で全文検索専用の構文を使わなけらず、素の Rails からではマイグレーションや検索などで多少見栄えが悪い。それを解決するために、Tritonn の場合は acts_as_tritonn、Hyper Estraier の場合は acts_as_searchable という plugin が存在する。

acts_as_triton

その名の通り Rails から Tritonn を使うための plugin。次のようにしてインストールする:

script/plugin install http://ryu.rubyforge.org/svn/acts_as_tritonn

更に、Tritonn の SourceForge.JP プロジェクトページなどから Tritonn のバイナリパッケージをインストールし、起動しておくこと。

データベースの準備

mysql コマンドを実行し、次のクエリでデータベースを作成する:

CREATE DATABASE tritonn_development DEFAULT CHARACTER SET utf8;

config/database.yml の development を次のように編集する:

development:
  adapter: mysql
  database: tritonn_development
  username: tritonn
  password:
  host: localhost
  encoding: utf8
  timeout: 5000

各種設定は各自の環境に合わせること。

次にデータベースを定義するためマイグレーションを書くのだが、ここでひとつ問題がある。activerecord-2.0.2/lib/active_record/connection_adapters/mysql_adapter.rb を見ると、create_table は次のようになっている:

def create_table(name, options = {}) #:nodoc:
  super(name, {:options => "ENGINE=InnoDB"}.merge(options))
end

ストレージエンジンが InnoDB になるようベタ書きされている。このため ActiveRecord のマイグレーションでは、MySQL の設定がどうなっていようがデフォルトでは InnoDB になってしまう。

activerecord-2.0.2/lib/active_record/fixtures.rb の RDoc には次のように書かれている:

# When *not* to use transactional fixtures:
(略)
#   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
#      Use InnoDB, MaxDB, or NDB instead.

つまり、transactional fixtures のためにトランザクションの無い MyISAM を避けデフォルトで InnoDB を使うようになっているのだろう。

しかし Tritonn では MyISAM を使う必要がある。デフォルトで InnoDB になってしまう設定を上書きするには、create_table に :options => "ENGINE = MYISAM" とすればよい。

まずモデルを作成する:

script/generate model paragraph

db/migrate/001_create_paragraphs.rb を編集する:

class CreateParagraphs < ActiveRecord::Migration
  def self.up
    create_table :paragraphs, :options => "ENGINE = MYISAM" do |t|
      t.column :body, :text, :null => false
    end
  end

  def self.down
    drop_table :paragraphs
  end
end

app/models/paragraph.rb を acts_as_tritonn に対応させる:

class Paragraph < ActiveRecord::Base
  acts_as_tritonn
end

次にこのモデルに全文検索のインデックスを張る。次のコマンドを実行する:

script/generate migration AddIndex

db/migrate/002_add_index.rb を編集する:

class AddIndex < ActiveRecord::Migration
  def self.up
    add_index :paragraphs, [:body], :fulltext => "NGRAM"
  end

  def self.down
    remove_index :paragraphs, :body
  end
end

最後にマイグレーションを実行する:

rake db:migrate

あとはそこにデータを入れるだけで自動的に全文検索用インデックスが更新される。

データの投入

サンプル文章を改行区切りで記述してあるファイル text.txt を Rails アプリケーションディレクトリ直下に置き、script/runner で動かすことのできる次のようなファイル、script/add_paragraphs.rb を用意する:

# -*- coding: utf-8 -*-

require 'benchmark'

puts Benchmark::CAPTION
bm = Benchmark.measure do
  open "text.txt" do |file|
    while line = file.gets
      line.chomp!
      paragraph = Paragraph.new()
      paragraph.body = line
      paragraph.save!
    end
  end
end
puts bm

そして次のようにコマンドを実行する:

script/runner script/add_paragraphs.rb

これで text.txt の中身を一行ずつ全文検索データベースに投入することができる。

データの検索

挿入したデータを検索する script/search_paragraphs.rb を準備する:

# -*- coding: utf-8 -*-

require 'benchmark'

Benchmark.bm do |x|
  x.report do
    paragraphs = Paragraph.find_fulltext({:body => "人間"})
  end
  x.report do
    paragraphs = Paragraph.find_fulltext({:body => "殺し"})
  end
  x.report do
    paragraphs = Paragraph.find_fulltext({:body => "残酷"})
  end
  x.report do
    paragraphs = Paragraph.find_fulltext({:body => "人間 殺し 残酷"})
  end
end

検索する文字列にはサンプルデータ中で一般的な単語を使用した。次のコマンドを実行することでこのスクリプトを呼び出すことができる:

script/runner script/search_paragraphs.rb

ページネーション

まず config/environment.rb の最後へ次の一行を追加すること:

require 'will_paginate'

find_fulltext をページネーションに対応させるため、app/controllers/application.rb の最後へ次のコードを追加する:

module WillPaginate
  module Finder
    module ClassMethods
      def paginate_by_find_fulltext(query, options)
        page, per_page, total_entries = wp_parse_options(options)
        WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
          options.delete(:page)
          options.delete(:per_page)

          count_options = options.dup
          count_options.delete(:select)
          unless pager.total_entries
            pager.total_entries = count_fulltext(query, count_options)
          end

          options[:limit] = per_page.to_i
          options[:offset] = (page.to_i - 1) * per_page.to_i
          pager.replace find_fulltext(query, options)
        end
      end
    end
  end
end

見た通り、WillPaginate::Collection.create を呼び、渡したブロックの中で pager.replace に検索結果を、pager.total_entries に結果件数を代入すればよい。

これで paginate_by_find_fulltext を使ってページネーションができるようになった。実際にページネーションを行うサンプルを以下に示す。

app/controllers/test_controller.rb

class TestController < ApplicationController
  def search
    @search_word = params[:search_word].to_s
    if @search_word != ''
      @paragraphs = Paragraph.paginate_by_find_fulltext(
        {:body => @search_word},
        :page => params[:page],
        :per_page => 10,
        :order => "id"
      )
    end
  end
end

app/views/test/search.html.erb

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta http-equiv="Content-Script-Type" content="text/javascript" />
  <title>検索</title>
</head>
<body>

  <% form_tag({:action => "search"}, {:onsubmit => "if (this.search_word.value.strip() != '') location.href='/'+encodeURIComponent(this.search_word.value.strip()).replace(/%20/g, '+').replace(/%2F/g, '%252F'); return false;", :method => "get"}) do %>
  <p>
    <%= text_field_tag "search_word", h(@search_word) %>
    <%= submit_tag "検索", :class => "submit" %>
  </p>
  <% end %>

<% if defined? @paragraphs %>
  <ul>
  <% @paragraphs.each do |paragraph| %>
    <li><%= h(paragraph.body) %></li>
  <% end %>
  </ul>
  <%= will_paginate @paragraphs, :prev_label => 'Prev', :next_label => 'Next' %>
<% end %>

</body>
</html>

config/routes.rb を以下のように書き換える:

ActionController::Routing::Routes.draw do |map|
  map.connect '/search', :controller => 'test', :action => 'search'
  map.connect '/search/*search_word', :controller => 'test', :action => 'search'
end

あとは script/server で起動し http://localhost:3000/search/ にアクセスし検索を行えばよい。あらかじめ script/add_paragraphs.rb でデータを投入しておけば、多くの検索結果がある場合にきちんとページネーションが行われているのを見ることができる。

acts_as_searchable

Rails から Hyper Estraier を使っての全文検索を提供する plugin。次のようにしてインストールする:

ruby script/plugin source svn://poocs.net/plugins/trunk
ruby script/plugin install acts_as_searchable

また Hyper Estraier のバイナリパッケージも入手しておくこと。

データベースの準備

mysql コマンドを実行し、次のクエリでデータベースを作成する:

CREATE DATABASE hyperestraier_development DEFAULT CHARACTER SET utf8;

config/database.yml の development を次のように編集する:

development:
  adapter: mysql
  database: hyperestraier_development
  username: root
  password:
  host: localhost
  encoding: utf8
  timeout: 5000
  estraier:
    host: localhost
    user: admin
    password: admin
    port: 1978
    node: development

各種設定は各自の環境に合わせること。

次に Hyaper Estraier のバイナリのあるディレクトリへ PATH を通した状態で Hyper Estraier のデータを置くディレクトリへ移動し、次のコマンドを実行する:

estmaster init test

Hyper Estraier を起動する:

estmaster start test

ウェブブラウザで http://localhost:1978/ を開き、administration -> Manage Nodes の順で開き、二つのテキスト入力欄を「development」と「node for development」で埋め、create を押す。これで全文検索のためのノードが生成された。

モデルを作成する:

script/generate model paragraph

db/migrate/001_create_paragraphs.rb を編集する:

class CreateParagraphs < ActiveRecord::Migration
  def self.up
    create_table :paragraphs do |t|
      t.column :body, :text, :null => false
    end
  end

  def self.down
    drop_table :paragraphs
  end
end

Hyper Estraier は MySQL とは別に存在するため、MySQL へのマイグレーションの際には特別なことをする必要が無い。

app/models/paragraph.rb を acts_as_searchable に対応させる:

class Paragraph < ActiveRecord::Base
  acts_as_searchable :searchable_fields => [:body]
end

これで body カラムに全文検索インデックスが張られる。

マイグレーションを実行する:

rake db:migrate

データの投入

acts_as_tritonn の時と同様に、サンプル文章を改行区切りで記述してあるファイル text.txt を Rails アプリケーションディレクトリ直下に置き、script/runner で動かすことのできる次のようなファイル、script/add_paragraphs.rb を用意する:

# -*- coding: utf-8 -*-

require 'benchmark'
require 'acts_as_searchable'

puts Benchmark::CAPTION
bm = Benchmark.measure do
  open "text.txt" do |file|
    while line = file.gets
      line.chomp!
      paragraph = Paragraph.new()
      paragraph.body = line
      paragraph.save!
    end
  end
end
puts bm

save! 時の全文検索インデックスの更新は plugin によって隠蔽されているため、acts_as_tritonn の時と全く同じソースコードを使うことができる。

コマンドの実行も同様に行う:

script/runner script/add_paragraphs.rb

データの検索

挿入したデータを検索する script/search_paragraphs.rb を準備する:

# -*- coding: utf-8 -*-

require 'benchmark'

Benchmark.bm do |x|
  x.report do
    paragraphs = Paragraph.fulltext_search("人間")
  end
  x.report do
    paragraphs = Paragraph.fulltext_search("殺し")
  end
  x.report do
    paragraphs = Paragraph.fulltext_search("残酷")
  end
  x.report do
    paragraphs = Paragraph.fulltext_search("人間 AND 殺し AND 残酷")
  end
end

acts_as_tritonn の場合と構文が大きく違うことに注意。スクリプトの呼び出しは変わらない:

script/runner script/search_paragraphs.rb

ページネーション

まず config/environment.rb の最後へ次の一行を追加すること:

require 'will_paginate'

fulltext_seach をページネーションに対応させる plugin があるので、それを利用する:

script/plugin install http://small-plugins.googlecode.com/svn/trunk/will_paginate_acts_as_searchable/

この plugin をインストールすることによって使えるようになる paginate_by_fulltext_search を使ったページネーションを行うサンプルを以下に示す。

app/controllers/test_controller.rb

class TestController < ApplicationController
  def search
    @search_word = params[:search_word].to_s
    if @search_word != ''
      @paragraphs = Paragraph.paginate_by_fulltext_search(
        @search_word,
        :page => params[:page],
        :per_page => 10,
        :order => "id"
      )
    end
  end
end

app/views/test/search.html.erb

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta http-equiv="Content-Script-Type" content="text/javascript" />
  <title>検索</title>
</head>
<body>

  <% form_tag({:action => "search"}, {:onsubmit => "if (this.search_word.value.strip() != '') location.href='/'+encodeURIComponent(this.search_word.value.strip()).replace(/%20/g, '+').replace(/%2F/g, '%252F'); return false;", :method => "get"}) do %>
  <p>
    <%= text_field_tag "search_word", h(@search_word) %>
    <%= submit_tag "検索", :class => "submit" %>
  </p>
  <% end %>

<% if defined? @paragraphs %>
  <ul>
  <% @paragraphs.each do |paragraph| %>
    <li><%= h(paragraph.body) %></li>
  <% end %>
  </ul>
  <%= will_paginate @paragraphs, :prev_label => 'Prev', :next_label => 'Next' %>
<% end %>

</body>
</html>

config/routes.rb を以下のように書き換える:

ActionController::Routing::Routes.draw do |map|
  map.connect '/search', :controller => 'test', :action => 'search'
  map.connect '/search/*search_word', :controller => 'test', :action => 'search'
end

Hyper Estraier を起動してから script/add_paragraphs.rb でデータを投入し、次に script/server で Rails アプリケーションを起動し http://localhost:3000/search/ にアクセスし検索を行えば、ページネーションが動作していることを確認することができる。

ベンチマーク結果

まず念のために言っておくと、これはそれぞれの plugin を通しての結果なので Tritonn 及び Hyper Estraier が持つ本来のパフォーマンスとは限らない。plugin を使わなくてもいいというのなら Tritonn や Hyper Estraier の複雑な構文をそのまま Rails で使うことで更に高速化する余地は残されている。

以下は、ディスク I/O を最小限にするため、ディスク上のファイルへあらかじめアクセスして HDD 内部や OS 側でのキャッシュを効かせた状態で計測している。

search_paragraphs.rb の動作結果:

acts_as_tritonn
      user     system      total        real
  0.063000   0.031000   0.094000 (  0.110000)
  0.015000   0.000000   0.015000 (  0.015000)
  0.000000   0.000000   0.000000 (  0.000000)
  0.000000   0.000000   0.000000 (  0.000000)

acts_as_searchable
      user     system      total        real
  0.000000   0.000000   0.000000 (  0.016000)
  0.000000   0.000000   0.000000 (  0.016000)
  0.000000   0.000000   0.000000 (  0.015000)
  0.000000   0.000000   0.000000 (  0.000000)

一番最初だけ 4 倍の差で acts_as_seachable の方が速い。しかし残りのうち二つでは acts_as_tritonn の方が速かったりするので、実運用上の様々な検索を実行した場合にはもっと違う、複雑な結果が出ると思う。実際に使う場面に応じて必要な計測を行い、適したものを使うべきだろう。

add_paragraphs.rb の動作結果:

acts_as_tritonn
      user     system      total        real
  4.328000   1.422000   5.750000 (  8.921000)

acts_as_searchable
      user     system      total        real
 10.844000   4.891000  15.735000 (168.829000)

ちょっと見過ごせない程の極端な差がついた。acts_as_seachable で save! の時に毎回インデックスを更新していることが問題になっているようだ。Hyper Estraier の User's Guide にはこうある。

可用性の確保

インデックスを更新している最中にはロックがかかるので、そのインデックスを使った検索はその間はできなくなります。検索システムとしては、その間は停止時間ということになります。それを避けるためには、インデックスのコピーに対して更新を処理を行い、完了したらオリジナルと入れ換えるようにするとよいでしょう。

Hyper Estraier では元々大規模な利用においてリアルタイムでインデックスを更新することを考えていないようだ。従って acts_as_seachable の実装に問題があるということになる。簡単に全文検索インデックスを付けたい場合に acts_as_searchable は使えるが、現状のままではある程度の更新が発生する大規模サイトに使うのは難しい。acts_as_searchable から更新時のリアルタイムインデックス更新機能を削り、任意のタイミングでインデックスを更新するように改造する必要がある。

acts_as_tritonn の場合にはこのような手間はよほど大規模にならない限り必要無いと思われるため、そのまま使うことができる。

text.zip
intern2008.jpg
こんにちは、研究開発部の溝口です。


弊社でも毎年恒例となりつつある技術系のインターンシップが始まりました。

今年のインターンシップに参加される学生さんは4名で、それぞれ2名づつ2チームに分かれ、ホストとなる社員とともに、研究開発本部のテーマに沿ったプロダクト開発を約1ヶ月にわたってフルタイムで行っていただきます。

今年のテーマは、やはりニコニコ動画関連です。

インターンの最後には社内で成果発表を行っていただきますし、その成果によっては世にでる可能性も十分にあります。

参加者のみなさんにはこの機会にネット・エンターテインメントの開発サイド、仕事としてのソフトウェア開発、多くのエンドユーザ様へのプロダクト・アウトについて経験していただけることを期待しています。

プログラミングキャンプ企業見学


独立行政法人 情報処理推進機構(IPA)/財団法人 日本情報処理開発協会(JIPDEC)が主催となって8月13日〜8月17日にかけて、「セキュリティ&プログラミングキャンプ2008」が開催されています。


そのプログラムの一部として、本日、プログラミングキャンプ参加者のみなさんが企業見学としてドワンゴまで来ていただきました。


そこでは、ドワンゴの成り立ちやビジネスの経緯、開発現場の実際や、サービスができるまでのステップなどをお話させていただきました。


またその後、社内見学ツアーと、ニコニコ動画を作った戀塚を含むドワンゴのエンジニアとの交流会も実施しました。


プログラミングキャンプ参加者の方は14歳〜22歳と若い方ばかりで、交流会に参加したエンジニアも参加者の方々のプログラミングを学ぼうという強い気持ちに刺激を受けたようです。


参加者のみなさんにも、弊社のサービス開発の実際や、エンジニアの文化などにふれていただけたかと思います。


この若い人たちのなかから、将来の「達人プログラマー」が沢山輩出されることでしょう。期待しています。

弊社エンジニア、戀塚昭彦が Adobe Max Japan 2007 における講演で使用した資料を公開します。

講演をお聞きくださった皆様に感謝いたします。

また先日開催されたイベント ITpro Challenge! での動画を ITpro 様に公開していただきました。

講演資料はこちらで公開しております。

ITpro様、ありがとうございました。

第2回モバイル勉強会

| コメント(0) | トラックバック(1)
「第2回モバイル勉強会」 (2007/9/17) で用いた発表用資料です。

Files

Powered by Movable Type 4.2rc5-ja

アイテム

  • intern2008.jpg
  • プログラミングキャンプ企業見学

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。