関口宏司のLuceneブログ

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

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

| スポンサードリンク | - | | - | - |
NGramTokenizerとEdgeNGramTokenFilter
Lucene 2.2のcontribにN-gramで単語を切り出すNGramTokenizerとEdgeNGramTokenFilterが追加された(同時にEdgeNGramTokenizerとNGramTokenFilterも追加されたが、これらはあまり使われない気がする)。

これまでLuceneのN-gram Analyzerといえば、CJKAnalyzerであった。これはCJK文字のときにだけ2文字単位で切り出すもので、bi-gramとも呼ばれるものである。

CJKAnalyzerのサンプルプログラムを以下に示す:



public class TestNgramAnalyzer {

private static Analyzer analyzer = new CJKAnalyzer();
private static final String TEXT = "メガネは顔の一部です。";

public static void main(String[] args) throws IOException {
TokenStream stream = analyzer.tokenStream( "F", new StringReader( TEXT ) );
StringBuffer sb = new StringBuffer();
for( Token token = stream.next(); token != null; token = stream.next() ){
//System.out.println( token.toString() );
sb.append( '[' ).append( token.termText() ).append( "] " );
}
stream.close();
System.out.println( TEXT + " => " + sb.toString() );
}
}



このプログラムを実行すると、次のようになる:



メガネは顔の一部です。 => [メガ] [ガネ] [ネは] [は顔] [顔の] [の一] [一部]
[部で] [です]



CJKAnalyzerは上記のように日本語の文字を2文字単位に切り出す。このとき、となりどうしのトークンの文字が互いに重なるように切り出すのがN-gramの特徴である。たとえば、「メガネ」は「メガ」と「ガネ」に分割される。こうすることで後に「メガネ」で検索するときにも「(メガ)(ガネ)」という成句で検索されるために文章中の「メガネ」がうまい具合に検索できるのだ。

CJKAnalyzerを使う動機としては、(形態素解析の)JapaneseAnalyzerと比較して辞書のメンテナンスが不要で流行語に強い、などが理由にあげられる。

一方、CJKAnalyzerを敬遠する理由としては、(Lucene本にも書いた例だが)「京都」で引いたときに「東京都」が引っかかってしまうとか(これはGoogleでもみられる現象だ)、1文字の単語が検索できない、などがあげられる。「京都」で「東京都」が検索できてしまうのは目をつぶれるとしても、後者は特に困った問題である。たとえば、先のサンプルの例文はCJKAnalyzerを使っているときは、「顔」という検索語では検索できない。

NGramTokenizer

NGramTokenizerはコンストラクタで指定した大きさのトークンを文章から切り出すTokenizerである。たとえば、次のように指定すると、bi-gramとして動作する:



new NGramTokenizer( reader, 2, 2 );



また次のようにすれば、3文字ずつ切り出すtri-gramとして働くようになる:



new NGramTokenizer( reader, 3, 3 );



これだけだとただ単にCJKAnalyzerがちょっと発展したくらいにしか思えないが、次のようにすることで1〜3文字を切り出すように動作するのがNGramTokenizerの便利なところだ:



new NGramTokenizer( reader, 1, 3 );



これをCJKAnalyzerの代わりに使用すれば、先ほどの「顔」で検索できない、という問題も解消できる。

具体的なプログラムを示すと、まずNGramTokenizerはAnalyzerではないので、使用するにはAnalyzerを作成する必要がある。簡単なAnalyzerは次のようなプログラムになるだろう:




static class MyAnalyzer extends Analyzer {
public TokenStream tokenStream(String fieldName, Reader reader) {
return new NGramTokenizer( reader, 1, 3 );
}
}



上記のMyAnalyzerを最初のプログラム例のCJKAnalyzerと入れ替えて使用する。そして実行すると、次のようになる:



メガネは顔の一部です。 => [メ] [ガ] [ネ] [は] [顔] [の] [一] [部] [で] [す] [。]
[メガ] [ガネ] [ネは] [は顔] [顔の] [の一] [一部] [部で] [です] [す。] [メガネ]
[ガネは] [ネは顔] [は顔の] [顔の一] [の一部] [一部で] [部です] [です。]



EdgeNGramTokenFilter

EdgeNGramTokenFilterの用途としてはおそらく、形態素解析で切り出された単語をさらにN-gramで分割するときに使用するものと考えられる。

先ほどのMyAnalyzerのプログラムをWhitespaceAnalyzerとEdgeNGramTokenFilterを使うように、次のように変更してみる:



static class MyAnalyzer extends Analyzer {
public TokenStream tokenStream(String fieldName, Reader reader) {
TokenStream stream = new WhitespaceTokenizer( reader );
return new EdgeNGramTokenFilter(stream, Side.FRONT, 1, 4 );
}
}



そして(WhitespaceAnalyzerなので)適当な英文を選んで最初のプログラムのTEXTに代入して実行すると、次のようになる:



The iPhone results sent A&T stock down slightly. => [T] [Th] [The] [i] [iP] [iPh
] [iPho] [r] [re] [res] [resu] [s] [se] [sen] [sent] [A] [A&] [A&T] [s] [st] [st
o] [stoc] [d] [do] [dow] [down] [s] [sl] [sli] [slig]



WhitespaceAnalyzerの代わりにJapaneseAnalyzerを使用すれば、日本語を形態素解析した後にそれぞれの単語をさらにN-gramで分割する、ということができるはずなので、興味のある人はやってみていただきたい。
| 関口宏司 | Lucene自由自在 | 01:15 | comments(0) | trackbacks(1) |
deprecatedとなったQueryFilter
ここ数回にわたってLucene 2.2の新機能を紹介してきたが(まだ紹介していない機能は別の回にまわす)、今回はLucene 2.2でdeprecatedとなった機能であるQueryFilterについて説明しよう。

QueryFilterはインデックスのドキュメントを「篩(ふるい)」にかけるFilterの一種で、同じFilterの一種であるCachingWrapperFilterのように、以前作成したBitSetオブジェクトを保存するキャッシュを内蔵するFilterである。Filter(抽象クラス)、QueryFilterおよびCachingWrapperFilterの関係はLucene 2.1までは図Aのようであった。

Lucene 2.1まで

Lucene本のP207-211にはそれぞれのFilterについて説明があるが、本を書いた当時は同じキャッシュのコードがQueryFilterとCachingWrapperFilterにあったため、美しくないなと感じた記憶がある。

Lucene 2.2でこの部分にリファクタリングが入り、QueryFilterはCachingWrapperFilterのサブクラスとなってQueryFilterそれ自身からはキャッシュのコードが取り除かれた。さらにQueryFilterはdeprecatedにマークされ、QueryWrapperFilterというFilterが新規に追加されて、Filterクラスは全体として図Bのようになった。

Lucene 2.2から

これにより、Lucene 2.2からは次のようなプログラムをコンパイルするとdeprecatedの警告メッセージが表示されるようになった:



Query q = ...;
Filter filter = new QueryFilter( q );



メッセージは警告なので無視してもかまわない。以前のプログラムは同じキャッシュの性能効率で動作することが保証されている。

警告メッセージが出ないよう以前のプログラムを直す場合は、QueryFilterを使っている部分をQueryWrapperFilterとCachingWrapperFilterを組み合わせた形で次のようにプログラミングすればよい:



Query q = ...;
Filter filter = new CachingWrapperFilter( new QueryWrapperFilter( q ) );



こうすれば、警告メッセージも出ず、プログラムも以前と同じ性能で動作する。

ところでFilterクラス群がこのように落ち着く前に、図Cのように単純にQueryFilterからキャッシュ機能を取り除く案が提案されていた:

キャッシュ機能のないQueryFilter

http://issues.apache.org/jira/browse/LUCENE-857

この案では、キャッシュのコードの冗長部分をただ単にQueryFilterから取り除いただけである。したがって、Lucene 2.1以前のQueryFilterを使ったプログラムがLucene 2.2でもキャッシュが効くようにするには次のようにプログラミングしなければならない:



Query q = ...;
Filter filter = new CachingWrapperFilter( new QueryFilter( q ) );



しかしこの案ではLucene 2.1以前のQueryFilterを使ったプログラムは警告メッセージも出ずにコンパイルできてしまい、気づかずにLucene 2.2にアップグレードしてしまった場合はキャッシュが効かなくなるので性能が格段に落ちてしまう。

そこで最終的に落ち着いた案は図Bのような形である。これにより、以前のプログラムをコンパイルするとdeprecatedの警告が表示され、新しい使い方にスムーズに移行を促すことができ、警告をとりあえず無視して運用しても性能を落とすことがなくなる。

コミッターのちょっとした気遣いが光ったissueといるといえるだろう。
| 関口宏司 | Luceneクラス解説 | 06:36 | comments(0) | trackbacks(0) |
CJKAnalyzerを使って強調表示を行う
梅雨の合間の、青空が気持ちのいいある平日。自宅でこのブログの記事を執筆しようとしたそのとき、電話が鳴った。ただ「はい」とだけ言い、受話器の向こうの声に耳を澄ます。最近の多くの人にも当てはまることだと想像するが、近頃私は自宅の電話に出るときに意識して名乗らないようにしている。かなりの確率でセールスの電話であったり、間違い電話で名乗ったとたんに無言で切られ、一部の個人情報を伝えてしまったことに納得のいかない思いをすることがしばしばだからである。

しかし電話は若い元気な女性の声で、

「いつもお世話になっております。リクルートの××です」と名乗った。

なぜ自宅に?という思いと同時に、仕事の電話かもしれないではないかという計算が働く。私も、

「お世話になっております。関口です」とそつなく返した。

しかしほどなくして、(こちらの)仕事になるような電話ではないことが判明した。電話の女性はタウンワークの者だといい、求人広告を掲載しませんか、と勧誘してきたのである。

この後笑い話となる二人の会話がしばらく成立したのは、第一に彼女の方に原因があることは間違いないが、私の方にもその布石となる出来事がその数時間前にあったのだった。

その出来事とは弊社丸の内オフィスにかかってきたやはり一本の電話である。それも果たして求人広告を出しませんか、という別の業者からの電話であった。

最近の私の頭の中にはいつも求人のことはあるにはある。しかし、日ごろの忙しさにかまけて実行動はなかなか取れていなかった。だから普段セールスの電話は歓迎しない私も(これも一種のセールスの電話であろう)、この機会にシステム(料金体系とか)を聞いてみようか、という気分にそのときはなっていた。そしてその業者から一通り説明を聞いた後「じゃあ、検討します」とお決まりの文句を言って電話を切ったのが数時間前のことだ。

そんなことがあったので、タウンワークからの電話を受け取ったときも「彼らの嗅覚も大したものだなあ、こんなタイミングで電話してくるなんて」と感心しただけだった。

「求人は考えていないこともないのですが、雑誌に広告を出すというほど大げさなことは考えていないのですが」と正直な気持ちをまず話した。

「はい、とりあえず、本日はお店におうかがいさせていただき、ごあいさつをさせていただきたいのですが、ご都合はいかがでしょうか」

オフィスをお店と表現するところなどさすがタウンワークである。へんな感心をしつつ、しかしここはお店でもオフィスでもなく自宅であることを思い出した。自宅にごあいさつにこられると面倒なので、とりあえず電話で聞けるだけの情報を聞きたいと思った私は、別の質問を続けることにした。

「しかし、タウンワークに求人出して人が来ますかね?」

「はい、たくさん来ますよー」

「そんなもんですかねー。広告出すといっても、ただじゃないんですよね?」

「はい、1万×千円です」

「なるほどー。それで期間的には?」

「一週間です」

「一週間ねー。それで人が来ますかね?」

「はい、これまで広告を出したことがなければ、絶対に効果があります!」電話の女性の声はますます元気だ。

しかし私は納得がいかなかった。なぜなら数時間前の別の求人広告業者のシステムの説明では、×十万円・半年間のWeb掲載で2〜3人紹介できますという話で、あたかも半年間で2〜3人紹介できることがすごいことのように話していたからだった。

Webの半年間掲載でそれだけなのに地域密着型のタウンワークにたったの一週間掲載でそんなに人が集まるものだろうか。なにかがおかしい、そう感じた私は、電話を取ったときに最初に感じた違和感に正対することにした。そして、このように尋ねた。

「そうですか。ところで、こちらの電話番号はどのようにお調べになったのでしょう?」

「はい、私は××と××の地域を担当しておりまして、以前お店におうかがいしたときに店長さんがいらっしゃらなかったので、本日改めましてお電話をさせていただきました!」

私は社長である前に一技術者であることに誇りを持って仕事をしている人間である。しかし名刺には社長と印刷してあるので、社長と呼ばれることもあり、その自覚もなくはない。しかし店長といわれたのは今回が初めてである。いよいよおかしい。そこで思い切って、さらに尋ねたのだった。

「そうですか。でもここはお店とかではなくて・・・。失礼ですがひょっとしてお間違いではありませんか?」

「あれ、サーティワンアイスクリームさんじゃないんですか」

「あー、ちがいますねー」PCの画面を見るともう十分くらい話している。なんということだろう。

「きゃー、失礼しました。すみませんが御社名はなんとおっしゃるのでしょうか」

「ロンウイットです」

「失礼しました。ロンウイット様、求人広告はいかがですか」

さすがリクルートの営業だ。この女性を採用しようか、ふとそんな思いがよぎったりもした。しかし弊社がまず欲しいのは営業ではなく技術者なので、その考えは思いとどまった。

その後私は弊社のような業種がタウンワークに広告を出して果たして人が集まるのか、それに対して女性は大丈夫、コンピュータ業界の会社もタウンワークで人を集めていますよ、という議論でさらに5分ほど費やし、「じゃあ、検討します」というやはりお決まりの文句で受話器を置いたのであった。はー。




さて、本題に話を移すが、今回は最近まで気がつかなかったLuceneの改善点について話そうと思って書き始めたのだった。それはHighlighterのことである。

これはLucene本にも書いたことが、CJKAnalyzerはHighlighterと組み合わせて使用することができない。しかしそれはLucene 2.0までの話で、Lucene 2.1からはCJKAnalyzerでもHighlighterが正しく動作するように改善されたのだった。うかつな話で、私はLuceneが2.2になった最近までそのことに気がつかなかった。

この修正は次のURLで参照できる:

http://issues.apache.org/jira/browse/LUCENE-627

では早速試してみよう。以下はCJKAnalyzerを使ったHighlighterのサンプルプログラムである:



public class TestHighlighter {

private static final String CONTENT = "ここに適当な文章を挿入";
private static final String QUERY_STRING = "ここに適当なキーワードを挿入";
private static final String F = "f";
private static Analyzer analyzer = new CJKAnalyzer();

public static void main(String[] args) throws ParseException, IOException {
QueryParser qp = new QueryParser( F, analyzer );
Query query = qp.parse( QUERY_STRING );
Scorer scorer = new QueryScorer( query, F );
Highlighter h = new Highlighter( scorer );
System.out.println( h.getBestFragment( analyzer, F, CONTENT ) );
}
}



上記の赤字の部分に適当な文章(長文)と検索キーワードをそれぞれ埋め込んで実行する。たとえば文章として適当に選んだニュース記事と検索キーワードに「赤城 事務所費」と記述してコンパイル・実行すると、Lucene 2.1以降では次のようになる:



の経常経費を計上していた点について、<B>赤城</B>農相は、水戸市にある自民党茨城県
第1選挙区支部<B>事務所</B>を使った後援会活動の経費も合算し、報告していると釈明
した。実家は「祖父の代から後援会活動の中核の場所で、会合を開いている」と強調。
<B>事務所費</B>



正しく強調表示されているのがわかるだろう。しかしLucene 2.0以前のバージョンでは、次のように強調表示が正しく動作しない。



、<B>赤城農相は</B>、水戸市にある自民党茨城県第1<B>選挙区支部事務所を使った後援
会活動の経費も合算し</B>、報告していると釈明した。実家は「祖父の代から後援会活動
の中核の場所で、会合を開いている」と強調。<B>事務所費については</B>


| 関口宏司 | Lucene自由自在 | 09:13 | comments(1) | trackbacks(0) |
Lucene 2.2のPre-analyzed Fields
Lucene 2.2にTokenStreamを引数にとる新しいFieldのコンストラクタが追加された。

これが何かを話す前に、まずこれまでのFieldのコンストラクタにはどんなものがあったのかおさらいしておこう。

まずはFieldの値にString型の引数をとるコンストラクタがあり、これは最もよく使われるFieldコンストラクタの基本形である。

このコンストラクタはFieldの値の文字列をインデックスに登録するかどうかをStore.YESまたはStore.NOで指定し、さらにその文字列を索引付けするかどうか、索引付けする場合に単語に分割するかどうかをIndex.NO、Index.UN_TOKENIZED、Index.TOKENIZEDで指定できるコンストラクタである。

String型の引数を取るFieldのコンストラクタが最も使われるので、ついつい忘れがちだが、この他にもReader型を引数にとるものとbyte[]型を引数にとるものがある。これらはString型のとは違い、StoreやIndexを指定しない。StoreやIndexの値はこれらのコンストラクタにおいてはあらかじめ決められている。

Readerの場合はIndex.TOKENIZEDで索引付けされ、その代わり登録はされない。byte[]のバージョンは(バイトストリームであるために)索引付けはされずStoreに関してはYESまたはCOMPRESSのどちらかが指定できる。byte[]のコンストラクタではStore.NOを選ぶとIllegalArgumentExceptionがスローされる。これは索引付けもせず登録もしないのは意味のない行為であるとみなされるためだ。

以上はLucene 1.9でも同様でありLucene本にも記載されている(P.91)。

Lucene 2.2ではこれらのコンストラクタに新たにTokenStream型の引数を取るコンストラクタが加わった。これは次のJIRAにファイルされた機能で、通称「Pre-analyzed Fields」と呼ばれるフィールドを生成するものである:

https://issues.apache.org/jira/browse/LUCENE-580

Pre-analyzedフィールド、つまりフィールドのインスタンスを生成する前にすでに文章が単語分割されている、ということである。Lucene 2.2の新機能として「フィールドのインスタンスを生成する前にすでに文章が単語分割されている」などという説明を読むとなんだかすごいことのように感じるが、なんのことはない。単語分割はプログラマが自分で行わなければならないのだ。どうやるかというと、AnalyzerクラスのtokenStream()メソッドを使ってTokenStreamを取得するのである。

ではこのコンストラクタはどんなときに使うと便利かというと、インデックスに登録する前に何らかの理由で文章を単語分割したい場面にたまに出くわすが、そのようなときに使うとよいだろう。たとえば私のある客先では、Fieldを作成する前に(別に後でもいいのだが)文章の単語数をカウントする必要があった。

Lucene 2.1当時はこのためにその文章をAnalyzerに通し、TokenStreamの出力から単語数をカウントしてTokenStreamは捨てていた。そしてFieldはFieldでその文章を使って作るので、二重にAnalyzerにかけることになって少々効率が悪かった。

このような場面でLucene 2.2ではTokenStreamは捨てずにそのままFieldのコンストラクタに渡して索引付けに使うことができるので効率がよくなる。もっとも、TokenStreamを引数にとるコンストラクタではStore.NOとなるので、文章を登録したい場合は使えないので注意が必要だ。

Lucene 2.2のFieldのコンストラクタをまとめると、次のようになる(TermVectorは省略):

StoreIndex
Stringコントロール可コントロール可
byte[]YESまたはCOMPRESSNO
ReaderNOTOKENIZED
TokenStreamNOTOKENIZED(済)

| 関口宏司 | Lucene自由自在 | 09:38 | comments(0) | trackbacks(0) |
+ Solrによるブログ内検索
+ PROFILE
1234567
891011121314
15161718192021
22232425262728
293031    
<< July 2007 >>
+ 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