関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
スポンサーサイト

一定期間更新がないため広告を表示しています

| スポンサードリンク | - | | - | - |
LuceneとSennaの比較:検索性能・・・?
Senna/LudiaとLucene/Solrを比較する資料を作ろうかと考え、ためしに負荷試験をやっているがこれがなかなか手ごわい。

Ludiaの性能が予想以上に出ないので資料化ができないのである。postgresql.confの値を変えたりしてみるがだめだ。

そもそもRDBMSは全文検索のために作られたエンジンではないので、私はもともと全文検索機能をプラグインするというアイディアには懐疑的だ。postgresql.confの値も全文検索機能の性能向上のためにいじる部分が少ないように思う。

同じ「クエリ」といっても、トランザクションをしっかり保証するRDBMSと性能第一の全文検索エンジンでは、エンジンの仕事量がぜんぜん違うはずなのだ。

負荷試験ではLudiaのレコード数を20分の1くらいに落としてようやくLucene/Solrと同程度の性能(レスポンス&QPS)が出るようにはなった。

これくらいの性能差であれば資料化できるかもしれないが、私のPostgreSQL(Sennaの性能ではなくPostgreSQLのせいだろう)のチューニング技量のせいかもしれない。なので資料化するのではなく性能比較ができるパッケージか何かを作成し、各自で比較してもらえるようにする方法に変えようかと考えている。

ということで負荷試験はちょっと中止にしよう。土曜の夕方からLudiaで夜通し検索&チューニングを模索して気がついたら日付が変わって日曜日だ。

疲労した頭で浮かんだのが次の久々の替え歌である。


〜 Ludiaユーザに捧げる詩 〜

サーチング・オールナイト
(もんた&ブラザーズ、ダンシング・オールナイトのメロディーで♪)

インストールはずむ心
やさしい日本語マニュアル
CREATE INDEXのあとで
無邪気にクエリしてる

Searchin' all night SQLにすれば
Searchin' all night トランザクションが開始する
Searchin' all night このままずっと
Searchin' all night セッションを閉じて

なにげない近傍検索ひとつ
それだけで崩れてしまう

あぶなげな検索エンジンと知らず
チューニングを手さぐりしてた

Tunin' all night SQLにすれば
Tunin' all night トランザクションが開始する
Tunin' all night このままずっと
Tunin' all night セッションを閉じて

このクエリで最後のテスト
どちらからともなくそう決めて
思い出をなぞるようにクリック
カットオーバー前の夜のように

Searchin' all night SQLにすれば
Searchin' all night トランザクションが開始する
Searchin' all night このままずっと
Searchin' all night セッションを閉じて

Tunin' all night SQLにすれば
Tunin' all night トランザクションが開始する
Tunin' all night このままずっと
Tunin' all night セッションを閉じて
| 関口宏司 | その他のOSS検索エンジン | 03:26 | comments(1) | trackbacks(0) |
LuceneとSennaの比較:近傍検索
Sennaは近傍検索ができるということなので、次のようにLudiaで試してみたがうまくいかなかった:



demo=> create table test (col text);
CREATE TABLE
demo=> insert into test values ('a b');
INSERT 0 1
demo=> insert into test values ('a 1 b');
INSERT 0 1
demo=> insert into test values ('a 1 2 b');
INSERT 0 1
demo=> insert into test values ('a 1 2 3 b');
INSERT 0 1
demo=> create index tidx on test using fulltext(col);
CREATE INDEX
demo=> select col,pgs2getscore(ctid,'tidx') from test where col @@ '*N5 "a b"' order by pgs2getscore;
col | pgs2getscore
-----+--------------
a b | 5
(1 row)
demo=>



*Nの数値を広げても"a b"しかヒットしなかった。

Ludiaのマニュアルを読むと、「シーケンシャルスキャンが選択された場合は近傍検索ができない」とあり、「シーケンシャルスキャンが選択されたときはエラーにすることが設定で可能」とある。たしかに上の場合はEXPLAINで調べるとシーケンシャルスキャンになっている。

シーケンシャルスキャンが選択されてしまうのはデータ件数が少ないからかも(?)と仮定し、ためしに別のテーブル(200万件超のレコードが入っている)でやってみると、見事に動作した。しかしこんなことでは本番システムでは安心して使えないだろう。

Luceneではこのような制限はもちろんなく、安全・簡単に近傍検索が行える。QueryParserを使った場合は、"a b"~5という具合に記述すればよい。

同じデータに対してのLuceneでの実行結果は次のようになる:



0.9710705 : a b
0.54932046 : a 1 b
0.44851825 : a 1 2 b
0.33987468 : a 1 2 3 b



コロンの左に表示されているスコアにも注目して欲しい。Luceneの近傍検索では単語同士がより近い文書ほどスコアが高くなる。

Senna/Ludiaでは単純にtfしかみないため、上記のデータの場合はすべて同スコアとなってしまう。
| 関口宏司 | その他のOSS検索エンジン | 12:42 | comments(0) | trackbacks(0) |
LuceneとSennaの比較:ストップワードの取り扱い
ストップワードだけからなる映画のタイトル一覧、というのを見つけた(というかMLに投稿されていた):

http://wunderwood.org/most_casual_observer/2007/05/invisible_titles.html

ほとんどの検索エンジンはストップワードをインデックスに登録しない。ストップワードは出現頻度が高く、通常検索には使用されない単語なので、そのような単語をはじくことでインデックスを「ヘルシー」に保つ効果がある。しかし上記の例のようなこともある。映画やDVDを検索するサイトを構築することを考えた場合、このようにストップワードだけからなる映画のタイトルが存在することをうっかり忘れると大変なことになるであろう。その映画はタイトルでは検索できないことになってしまう。もちろん、どんな単語をストップワードとするかは自由に定義できるが、この例での根本的な解決にならない。

Luceneではストップワードの定義やストップワード自体の使用有無はAnalyzerごとに自由に決めることができる。そしてFieldごとにAnalyzerは自由に選ぶことが可能だ。したがって、ストップワードだけからなる可能性があるタイトルフィールドにはストップワードを使わないAnalyzerを適用する、と決めることが可能である。「タイトル」のようなフィールドは短い文なので、それだけストップワードも説明文にある同じ単語よりも存在意義(価値)があると考えられる。したがってタイトルにはストップワードをあえて定義しない、という選択は納得できる。

Senna(あるいは最近試用中のLudia)ではもっと話は簡単のようだ。なぜならストップワードという考え方自体がないからである(私の調べ方が甘い可能性があるので間違っているかもしれない)。Sennaではこういったことを何も考えずに投入しても、安全に上記の映画タイトルを索引付けして検索することが可能である。
| 関口宏司 | その他のOSS検索エンジン | 18:21 | comments(0) | trackbacks(0) |
off topic: ファミレスにて
私のその日の最後のミーティングは新橋のファミリーレストランで夜の8時から行われた。

案の定、ちょうどいい食事タイムのために店はひどく混雑していた。しかしざっと観察したところ、ファミリーレストランの名前にふさわしいグループはいなかった。

なにしろ新橋である。休前日の夜である。ファミリーは入りづらいのだった。

ちなみに東京になじみの薄い読者のために新橋を説明しておくと、日本初の鉄道が開通した駅があるところである、という紹介はここでは適当ではない。むしろ中高年サラリーマンの街としてテレビ・雑誌などで取り上げられることが多い街であると申し上げておきたい。

テレビ・雑誌などでの取り上げられ方としては、街頭インタビュー的なものがよくある。インタビューの題材としては政治問題から経済関連、教育問題から家庭問題、ときにはスポーツ&芸能関係までにもおよび、新橋のサラリーマンとして生きていくには膨大な知識が要求されることがこのことからもうかがえるだろう。しかし彼らはひるむことなくインタビューに応える。なぜなら彼らはたいてい酔っ払っているからだ。

そんな街新橋にあるファミリーレストランというのはどうだ。どうだと聞かれても困るかもしれないが、私は想像する。新橋のファミリーレストランで家族連れが食事をするのを。しかしそれは楽しい会話が弾む家族団欒とは程遠いのだった。

なにしろそこは休前日の夜の新橋だ。お父さんはもちろんできあがっている。酔っ払いのお父さんのイメージは私にとってそれは星一徹である。星一徹というとマンガ「巨人の星」の星明子・飛雄馬姉弟の父親であり食事のシーンではちゃぶ台をひっくり返す技を家族の前で披露することで有名である。しかし子供らはたまったものではない。そんな星ファミリーのような家族連れはいないか探したが、いなかったのでほっと胸をなでおろした。

そんなことを考えながら10分くらい待っていると、席が空いて案内された。

ちょど夕食時なのにもかかわらず、われわれは食事をとる時間を惜しむようにドリンクバーだけを頼んでミーティングを開始した。なぜそんなところでミーティングをしているかというと、有志が集まって自分たちのソフトウェア開発プロジェクトをやろうという、そのキックオフミーティングだからである。スポンサー(顧客)がいないのでそんなところでやるはめになったのだった。

こんなとき、自分がソフトウェア開発者であったことを感謝せずにはいられない。現代のソフトウェア開発はPCさえあれば開発環境はほとんど無料でそろえることができる。これがソフトウェア開発でなかったらどうだろう。

たとえば私は大工になった自分を想像する。新橋のファミリーレストランに集まったのはいずれもその道十年選手の左官屋と内装屋である。そろそろ一本立ちを意識し始めるころだ。しかし棟梁の私は煙管を片手に(席はいうまでもなく喫煙席だ)想うのだった。「職人の世界はそんなにあめぇもんじゃねぇよ」。しかしもちろん声には出さない。

ファミレスではそれぞれに開発したい夢のマイホームプランを語るだろう。職人といえども工務店や孫請け建設会社に勤める者たちだ。顧客からの要求は高く、開発予算は低い(どこの世界も同じということか)。その分、自分たちのプロジェクトにはいい材料をふんだんに取り入れたい。ところがいざ実装フェーズになると材料費がどうしても必要になってくる。結局、夢は夢のまま終わってしまうのだった。

しかし私は幸いにもソフトウェア開発者なのでそんな心配とは無用である。心配は別のところにあったのだった。それは仕事以外のそんなことをやっている時間が自分たちにあるのか、ということだ。しかし成長している会社は仕事以外の活動を社員に奨励しているということも聞く。それはたとえばGoogleである。Googleでは聞くところによると20%ルールというものがあり、勤務時間の20%を仕事以外の作業に当てなければならない、というものである。それは権利というよりもむしろ義務のようである。20%の義務というと一応経営者でもある私は(会社と個人の合計で)所得の20%の社会保険料を問答無用でもっていかれる、という楽しくない連想がどうしても働いてしまう。しかもGoogleの常に先を行きたい私にとって、20%というのはいずれにしろ妥協できない割合である。ここは迷わず30%ルールで行くことにする。

ミーティングではそれぞれの夢や妄想が語られ、高揚感を引きずりながらそれぞれ帰路についた。いいかげん空腹の私は、自宅近くの別のファミリーレストランで遅い夕食と、そしてその日2度目となるドリンクバーを注文したのだった。
| 関口宏司 | その他(分類不能) | 03:32 | comments(0) | trackbacks(0) |
LuceneとSennaの比較:スコア計算
最近全文検索エンジンLudia(1.4.0)を触る機会を得た。LudiaはSennaのPostgreSQLバインディングである(念のため)。

Ludiaでのクエリーは、演算子@@を使ったSQLを投げればいいだけの簡単操作である。

たとえば次の例はauction_itemというテーブルのdescriptionカラムに「音楽」と「ビデオ」の両方の単語を含むレコードを「全文検索」で検索するSQLを発行している。ただし、結果をスコア順およびauction_id順でソートした結果の最初の1件のみを取得している:



demo=> select auction_id,title,pgs2getscore(ctid,'fidx') from auction_item where
description @@ '*D+ 音楽 ビデオ' order by pgs2getscore desc,auction_id limit 1;
auction_id | title | pgs2getscore
------------+-----------------------------------------------------------+--------------
s62908788 | ★SONYソニーハードディスクオーディオレコーダーNAC-HD1新品 | 40
(1 row)
demo=>



なお、auction_itemのカラムdescriptionには全文検索インデックスfidxがあらかじめ作成されている。

ところでSennaについて前から気になっていることをこの結果を見てまた思い出した。それは検索結果のスコアのことだ。Sennaではどういう計算をしてスコアを算出しているのか前から気になっていたのだった。

上記のSQLで検索結果の全件を表示させてみると(SQLの最後の"limit 1"をはずす)、スコアは40がしばらく続き、ついで30がしばらく続き、25が続き、20が続き、・・・というような結果が得られる。この根拠はなんだろう。

ところでLuceneではスコアの計算式はSimilarityクラスのJavadocで紹介されている:

http://hudson.zones.apache.org/hudson/job/Lucene-trunk/javadoc/org/apache/lucene/search/Similarity.html
Luceneのスコア計算式


このような計算式をSennaのマニュアル上でもぜひ見たいところである。しかしGoogleでSennaのスコアの計算式を調べても出てこない。それどころかSennaのスコア算出の根拠を気にしているユーザがあまりいなさそうなのは私にとっては驚きだ。なぜなら検索結果の順番は、顧客からもっとも問い合わせの多い項目のひとつだからである。

Sennaはソースコードをオープンにしているので、ソースを丹念に調べればスコア算出の根拠も自分で調べられる、といわれれば確かにそのとおりだ。なので一応ソースを覗いてみる。ソースをダウンロードしてgrep scoreとすると、次のような行を見つけることができた:



:
./lib/query.c: *score += w * tf;
./lib/query.c: *score += w * tf;
./lib/query.c: *score += w * tf;
:



他にもいくつかの行がヒットしていたのでここだけではないと思うが、とりあえず上の行だけ見るとなんらかの重み(?・・・"w"なので・・・)にtfを掛けてそれをスコアに足しこんでいるようである。idfはSennaでは考慮しないのか?と思いGoogleで"senna idf"で調べると今度はひっかかり、Sennaのマニュアルに次の説明を見つけることができた:


sen_sel_similar
stringをわかち書きした語のうち、idf値が大きなsimilarity_threshold個の語のいずれかを含むレコードを検索します。


どうやら設定によりidfが考慮されるようである。ということは、デフォルトの状態ではtfのみがスコア計算のパラメータとなるのだろうか。早速確かめてみよう。

Luidaで次のSQLを実行して検索対象文書となるレコードを作成する:



demo=> create table test (col text);
CREATE TABLE
demo=> insert into test values ('a c a d a e');
INSERT 0 1
demo=> insert into test values ('a a a a a a');
INSERT 0 1
demo=> insert into test values ('a b');
INSERT 0 1
demo=>



次に転置索引を作成する:



demo=> create index tidx on test using fulltext(col);
CREATE INDEX
demo=>



この状態で「aまたはb」(OR検索)を含む文書を全文検索してみよう。すると次のようになる:



demo=> select col,pgs2getscore(ctid,'tidx') from test where col @@ '*DOR a b'
order by pgs2getscore desc;
col | pgs2getscore
-------------+--------------
a a a a a a | 30
a c a d a e | 15
a b | 10
(3 rows)
demo=>



予想通りtfのみが考慮された結果、aを最も多く含む文書がトップに表示され、その次にaをたくさん含む文書が2番目になり、最後にやっとbを含む文書が表示された。このデフォルトの動作はユーザを困惑させると思うのだが、どうだろう。

具体例にたとえるならば、「アップル または コンピュータ」というクエリを発行したユーザの気持ちとしては、アップルコンピュータのページが果物屋のページよりも先に表示されて欲しい、ということである。

ところがSennaのデフォルト設定では(いや、これはLudiaのデフォルト設定と言い換えたほうがいいかもしれない)、アップルが多数登場する果物屋のページが上位表示されてしまうのである。

Luceneではどうなるかというと、この例の場合はidfが大きな役割を果たし、bという単語の希少価値がスコアに大きく影響する。その結果、「a b」が最も上位に表示されるようになる。ついでtfの大きな「a a a a a a」となり最後に「a c a d a e」となる。つまりLuceneでは「アップル または コンピュータ」というクエリを発行したユーザは、「アップル」という単語が多数登場する果物屋のページに惑わされることなくアップルコンピュータのページに先にたどり着けるわけである。

これを実際に確かめるコードを次に示す:



public class TestScore {

static Directory dir;
static Analyzer analyzer = new WhitespaceAnalyzer();

public static void main(String[] args) throws IOException, ParseException {
dir = new RAMDirectory();
makeIndex();
searchIndex();
}

static void makeIndex() throws IOException {
IndexWriter writer = new IndexWriter( dir, analyzer );
writer.addDocument( getDocument( "a c a d a e" ) );
writer.addDocument( getDocument( "a a a a a a" ) );
writer.addDocument( getDocument( "a b" ) );
writer.close();
}

static Document getDocument( String value ){
Document doc = new Document();
doc.add( new Field( "f", value, Store.YES, Index.TOKENIZED ) );
return doc;
}

static void searchIndex() throws IOException, ParseException {
IndexSearcher searcher = new IndexSearcher( dir );
QueryParser qp = new QueryParser( "f", analyzer );
Query query = qp.parse( "a b" );
Hits hits = searcher.search( query );
for( int i = 0; i < hits.length(); i++ ){
Document doc = hits.doc( i );
float score = hits.score( i );
System.out.println( score + " : " + doc.get( "f" ) );
//Explanation explain = searcher.explain( query, hits.id( i ) );
//System.out.println( explain.toString() );

}
searcher.close();
}
}



実行結果は次のとおり、「a b」の文書が最も高いスコアを獲得する:



0.98479235 : a b
0.14789723 : a a a a a a
0.10457913 : a c a d a e



一部の顧客は検索結果の順位についてひどく敏感である。たとえばスポンサー企業から掲載料を受け取って検索結果としてその企業情報や商品などを表示するサービスを提供している顧客などだ。このようなときはその検索結果の順位についてきちんと根拠を説明できないといけない。場合によっては「10位に表示されてしまっている企業情報を1位に表示する」よう要望されることもある。そのためにはまず、現状のスコア値の根拠がなんなのかを知ることが絶対に必要になってくる。

こんなときに便利なのがLuceneのExplanationクラスである。前記の赤字部分のコメントアウトをはずして再度実行すると、次のようなスコア計算の根拠が表示される:



0.98479235 : a b
0.98479235 = (MATCH) sum of:
0.20126262 = (MATCH) weight(f:a in 2), product of:
0.4520737 = queryWeight(f:a), product of:
0.71231794 = idf(docFreq=3, numDocs=3)
0.63465154 = queryNorm
0.4451987 = (MATCH) fieldWeight(f:a in 2), product of:
1.0 = tf(termFreq(f:a)=1)
0.71231794 = idf(docFreq=3, numDocs=3)
0.625 = fieldNorm(field=f, doc=2)
0.78352976 = (MATCH) weight(f:b in 2), product of:
0.8919806 = queryWeight(f:b), product of:
1.4054651 = idf(docFreq=1, numDocs=3)
0.63465154 = queryNorm
0.8784157 = (MATCH) fieldWeight(f:b in 2), product of:
1.0 = tf(termFreq(f:b)=1)
1.4054651 = idf(docFreq=1, numDocs=3)
0.625 = fieldNorm(field=f, doc=2)



Lucene本にはtf、idf、lengthNorm(≒fieldNorm)、boostなどの解説がある。また、プログラムを実行できる環境をもっていない方は、弊社デモサイトの「スコア計算デモ」も参考になるだろう。
| 関口宏司 | その他のOSS検索エンジン | 01:43 | comments(0) | trackbacks(0) |
[警告] Lucene 2.3.1の不具合
Lucene 2.3.1の以下の不具合が見つかり、改修したバージョンの2.3.2がリリースされるかもしれない。

OOM時はコミットしないようにする
https://issues.apache.org/jira/browse/LUCENE-1191

TermVector利用時のIndexWriterによる不正なフラッシュタイミング
https://issues.apache.org/jira/browse/LUCENE-1197

DocumentsWriter.ThreadState.initメソッド実行時の例外がインデックスを破壊する
https://issues.apache.org/jira/browse/LUCENE-1198

IndexModifier.close()実行時のNPE
https://issues.apache.org/jira/browse/LUCENE-1199

IndexWriterのデッドロック問題
https://issues.apache.org/jira/browse/LUCENE-1200
https://issues.apache.org/jira/browse/LUCENE-1208
https://issues.apache.org/jira/browse/LUCENE-1210
| 関口宏司 | Luceneリリース | 02:51 | comments(0) | trackbacks(0) |
deleteDocuments(Query)メソッドのIndexWriterへの追加(2.4)
またまたLucene 2.4の話題だ。こうも続くと今の最新バージョンが2.4なのではないかと自分に暗示をかけてしまいそうだが、今のリリースはLucene 2.3.1である。

なぜこのブログで2.4の話題が続くかというと、ここ最近(特にIndexWriter周りで)更新が激しく、片っ端から書いていかないと2.4リリース時に追いつかなくなる恐れがあるからだ(といって、書かなくても別に誰かに怒られるわけではないのだが)。

・・・と一応断りを入れてLucene 2.4の新機能を紹介する。それはIndexWriterクラスにdeleteDocuments(Query)というメソッドが追加されたことである。これは引数で指定したQueryにヒットするDocumentを削除するメソッドである。引数にQuery[]をとるバージョンもある。

Lucene本を書いたLucene 1.9の時代は、Docunmentを削除するにはIndexReaderを使うしかなかったが、いまやIndexWriterだけで追加・更新・削除できるようになった。

IndexReaderには引数にDocumentの(int値の)IDを渡して削除するメソッドが(今でも)ある。同じことを実現するのはセグメントマージのたびにIDが変わってしまうIndexWriterでは難しいのでその代わりに提供されたのが今回のメソッドである。

IndexReaderがオープン中は、そのセグメントは固定されているのでIDを指定することは容易だが、IndexWriterではそうはいかないのだ。そのためIndexWriterを使ってのDocumentの削除はIDを指定して、というわけにはいかない(そもそも今作成中のインデックスのDocumentのIDをIndexWriterの外から知るすべがないだろう)。どうしても特定の1つのDocumentを削除したい場合は、ユニークキーをDocumentに持たせて、それをヒットさせるQueryを作成してdeleteDocuments(Query)で削除すればよい。これはSolrで使われている方法でもある。
| 関口宏司 | Luceneクラス解説 | 23:32 | comments(0) | trackbacks(0) |
検索タイムアウトのサポート(2.4)
これもまた現trunkの話だが、検索実行時のタイムアウトが設定できるようになった。これにより、アプリケーションは検索の完了を待たずにユーザにタイムアウト時点までで得られた検索結果を返すことができる。レスポンスタイムに厳しい制限のあるアプリケーションには朗報かもしれない。

使う場合は、低レベルAPIのHitCollectorを使用して次のように行う:



IndexSearcher searcher = new IndexSearcher( INDEX );
TopDocCollector tdc = new TopDocCollector( 100 );
TimeLimitedCollector tlc = new TimeLimitedCollector( tdc, timeout );
try{
searcher.search( query, tlc );
}
catch( TimeExceededException e ){
System.out.println( e );
}
System.out.println( "numFounds = " + tdc.getTotalHits() );
searcher.close();



上記の赤字部分にミリ秒単位のタイムアウト時間を設定する。

たとえば、40ミリ秒のタイムアウト時間を設定してタイムアウトが起こったときは、次のようになる:



org.apache.lucene.search.TimeLimitedCollector$TimeExceededException: Elapsed time: 60Exceeded allowed search time: 40 ms.
numFounds = 23744



上記の実行例では実際は27,000件のヒット件数があるクエリを発行したが、途中までの23,744件が得られたことになる。

なお、デフォルトでタイムアウト計測の単位が20ミリ秒となっている。これを変更するには、setResolution()メソッドで行う(ただし、5ミリ秒未満は指定できない)。
| 関口宏司 | Luceneクラス解説 | 09:48 | comments(0) | trackbacks(0) |
+ Solrによるブログ内検索
+ PROFILE
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
<< March 2008 >>
+ LINKS
検索エンジン製品 - 比較のポイント
商用検索エンジンを購入した企業担当者は読まないでください。ショックを受けますから・・・
>>製品比較 10のポイント
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
Apache Solr入門 ―オープンソース全文検索エンジン
Apache Solr入門 ―オープンソース全文検索エンジン (JUGEMレビュー »)
関口 宏司,三部 靖夫,武田 光平,中野 猛,大谷 純
+ RECOMMEND
Lucene in Action
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless
FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
qrcode
+ SPONSORED LINKS