関口宏司のLuceneブログ

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

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

| スポンサードリンク | - | | - | - |
ブログ検索のデモ公開
「医者の不養生」ということばがある。意味は「患者に養生を勧める医者が、自分自身は健康に注意しないこと。転じて、正しいとわかっていながら、実行が伴わないこと。(「ことわざデータバンク」より)」ということだ。

私は顧客にLucene導入を勧めるコンサルタントであるが、自社のホームページには「検索窓」を設置していない。これなどはまさに「医者の不養生」の身近な例かもしれない。しかし言い訳をさせてもらえば、まだ10ページくらいしかコンテンツがないので検索してもしようがないじゃないか、というのが本音である。

弊社のホームページの制作はデザイナーさんに外注しているが、彼は自分のホームページを持っていない。そういう意味で彼もまた「医者の不養生」仲間である。つまり不養生なデザイナーが別の不養生なコンサルのホームページを制作しているわけで、弊社のホームページは考えてみればとんでもない者どもが制作・運営しているということになっているのであった。

これはまずいのではないか。

しかし、ホームページに「検索窓」を設置するためだけに意味のないページを増やすのはなんだし、かといってページが増えるのを待っていたらいつになるかわからない。

そこで考えたのが今回紹介するブログの検索デモシステムである。

このブログのページの右端にある「ブログ内検索」の検索窓や以下の専用ページからこのブログの検索ができる。

関口宏司のLuceneブログ - ブログ検索デモ
http://demo.rondhuit-search.com/etclbsdemo/

ブログ検索は、検索結果がブログの記事単位で表示されるところが一般的なインターネット検索とは異なる。

一般的なインターネット検索では、ブログ記事がひっかかってもその記事はすでに過去のものとなっている可能性があり、検索結果をクリックしてブログのページに移動しても、ブログページの構成の性質により見つけたい記事そのものを見つけにくかったりする。

最近はブログに特化した検索サービスをインターネット検索業者が提供してきている。今回開発したデモは、基本的にはGoogleやYahoo!のブログ検索と同じしくみであるが、私のブログだけをクロールしてインデクシングをしているのがそれらのブログ検索と大きく異なっている点である。

デモシステムは次のようなしくみとなっている:

ブログ検索デモシステムの構成

デモのシステムとしては、まず「Luceneブログ」を記事単位にクロールしてインデックスに登録する「ブログクローラ」がある。そして、クライアントのブラウザから検索リクエストを受信して検索を実行し、検索結果一覧を返す「検索サーブレット」がある。さらに、新規の記事を投稿したときにブログサーバからPINGを受信し(ブログの管理画面にPING送信先を追記する必要がある)、先の「ブログクローラ」をキックする「PING受信サーブレット」という3つのプログラムからなっている。

まあ、このブログ自身、記事数が数十オーダーなので検索の母数とするにはまだまだだが、ブログの記事単位検索は使ってみるとインターネット検索よりははるかに便利なので、このブログの中だけを調べたいときはぜひ活用していただきたい。

| 関口宏司 | Lucene自由自在 | 14:09 | comments(0) | trackbacks(0) |
Lucene+Ajaxサンプルがエラーになる場合の対処法
読者からの指摘でLucene本の「6.6 Lucene+Ajax=インクリメンタルサーチ!」のサンプルの実行で「Java 1.4+Tomcat 5.0.28は動作するが、Java 5.0+Tomcat 5.0.28のときにJavaScriptエラー(図)が発生して動作しない」というのがあった。

page error

page error detail

調べたところ、サンプルで使用しているDWRの部分でエラーが発生しており、DWRのページによるとTomcatでXalanを正しくセットアップしていないことが原因であるようだ。

私の環境では、次のようにしたらJava 1.4でも5.0でもどちらでも動作したので、参考にしていただきたい:

1.$CATALINA_HOME/common/endorsedディレクトリにあるxercesImpl.jarとxml-apis.jarを削除。
2.http://www.apache.org/dyn/closer.cgi/xml/xerces-j/から最新のXerces-Jをダウンロード。
3.ダウンロードしたファイルを解凍し、xercesImpl.jarとxml-apis.jarを$CATALINA_HOME/common/libにコピー。

| 関口宏司 | 書籍「Apache Lucene入門」 | 09:52 | comments(0) | trackbacks(0) |
TermDocsとTermEnum
こう見えてもそれなりに忙しい私が年内中に片付けたいこととして、仕事場の掃除があげられる。ぱっと見はそれなりに片付いている感がある。しかし、必要なときにちょっとした文具やツールが見つからなかったりする。そんなとき、「Luceneの替え歌」を頭の中でリフレインしつつ探してみたりするのだが、見つからないものはやっぱり見つからない。

特に悲惨な状態になっているのが、机の「一番上の引き出しの中」である。私の使っている独立ワゴン型の引き出しは3段になっていて2段目と3段目はそれなりに深さがあり、A4のファイルや技術書が見やすく並べられ整理されている。しかし、一番上の引き出しはそれらに比べて浅い。いったい何を入れればいいのだろう。ITOKIの狙いがよくわからない。印刷業者から届けられた名刺ケースを引き出しの奥に置いてみる。しかしまだまだスペースが空いている。そこで印鑑と朱肉を配置する。まだ空いている。ホッチキスとその針を置く。まだまだ空いている。セロハンテープと液状ノリ。まだまだだ。目薬と爪切り。いろいろな大きさと色のポストイット。USBメモリ。レターオープナー。レーザーポインター。・・・こんな感じで一番上の引き出しは大変なことになっていくのである。

Luceneの全文検索インデックスも使い方によっては容易に混乱状態を作り出してしまう。RDBMSと違い、明確なスキーマ定義を持つ必要がないので、きちんと管理をしておかないといろいろな属性を持った文書が混在する(Apache Solrを使えばスキーマが管理できる。Solrについては別の機会に書きたい)。もちろん、きちんと設計したシステムでは、ヘテロな文書を混在させるのは意図して行ったことであり、正しい使い方で、また、しばしば行われている。たとえば、データベースにある構造化データと、HTMLやPDFのような非構造化データを横断的に検索したい場合、ひとつのインデックスにすべての文書を登録するが、種別が異なる文書では属性を統一できなかったりする。

フィールドの値ではなく、フィールドそのもので検索する

このような時、「フィールドの値ではなく、フィールドそのもので検索するにはどうすればいいか」という要件が出される場面に遭遇する。たとえば、「住所」というフィールドがある文書はすべてその値に関わらず検索したいとしよう。どうすればいいか。

まず簡単に思いつくのは、すべての文書タイプをきちんと整理し、同じ属性名からなる文書に同一の文書タイプを割り振って、その文書タイプもまた文書の属性として登録する方法である。こうすれば、文書タイプフィールドに対する検索に置き換えることができる。また似たような方法として、文書が持つ属性名のリストを属性として登録してもよい。そうすれば「属性名リスト」フィールドに対する属性名の検索で当初の目的が達成できる。

しかし、このような属性名検索のために新しいフィールドを設けるやり方をとらなくてもTermDocsを使えば特定のフィールドを持つ文書を見つけることができる、という方法が少し前のメーリングリストで流れていた。

TermDocsはseek()メソッドにTerm(フィールドと単語の組み合わせ)を与えると、それ以降、next()がtrueを返す間は文書とその文書に含まれる当該Termの数のペアを返すEnumeratorのインタフェースである。TermDocsのインスタンスはIndexReader.termDocs()で取得できる。

これで返されたTermDocsのseek()に、検索したい属性名と空文字列のTermを渡す。空文字列を渡すところがコツでこれにより当該属性を持つすべての文書が列挙できる・・・というのだが本当だろうか。

以下はサンプルプログラムである。異種文書が混在登録されているインデックスから、「住所」フィールドを持つ文書を表示しようとしている。



public final class SearchField {

static final String F_NAME = "名前";
static final String F_ADDR = "住所";
static final String F_AGE = "年齢";
static final String F_TYPE = "種別";
static final String F_NUMBER = "番号";
static final String F_LEVEL = "レベル";

public static void main(String[] args) throws IOException {

//---------------------------------------------------
// 異種混在ドキュメントの定義
//---------------------------------------------------
Source[] docSources = {
new Employee( "山田太郎", "東京都千代田区丸の内", "25" ),
new Company( "株式会社山田", "東京都足立区平野", "製造" ),
new Partner( "0123", "パートナーA", "A" ),
new Employee( "鈴木一郎", "東京都港区新橋", "30" ),
new Company( "株式会社鈴木", "東京都板橋区千川", "流通" ),
new Partner( "0234", "パートナーB", "B" ),
new Employee( "佐藤三郎", "東京都江戸川区江戸川", "35" ),
new Company( "佐藤有限会社", "東京都目黒区中町", "サービス" ),
new Partner( "5566", "パートナーC", "C" )
};

//---------------------------------------------------
// 異種混在ドキュメントのインデックスへの登録
//---------------------------------------------------
Directory dir = new RAMDirectory();
//Directory dir = FSDirectory.getDirectory( "index", true );
IndexWriter writer = new IndexWriter( dir, new JapaneseAnalyzer(), true );
for( Source docSource : docSources ){
Document doc = docSource.getDocument();
writer.addDocument( doc );
}
writer.close();

//---------------------------------------------------
// 「住所」フィールドを持つ文書を表示する・・・?
//---------------------------------------------------
IndexReader reader = IndexReader.open( dir );
TermDocs termDocs = reader.termDocs();
termDocs.seek( new Term( F_ADDR, "" ) );
while( termDocs.next() ){
Document doc = reader.document( termDocs.doc() );
int freq = termDocs.freq();
System.out.println( "doc: ¥"" + doc.toString() + "¥"¥tfreq: " + freq );
}
reader.close();
dir.close();
}

static interface Source {
public Document getDocument();
}

static class Employee implements Source {
String name;
String addr;
String age;
Employee( String name, String addr, String age ){
this.name = name;
this.addr = addr;
this.age = age;
}
public Document getDocument(){
Document doc = new Document();
doc.add( new Field( F_NAME, name, Field.Store.YES, Field.Index.TOKENIZED ) );
doc.add( new Field( F_ADDR, addr, Field.Store.YES, Field.Index.TOKENIZED ) );
doc.add( new Field( F_AGE, age, Field.Store.YES, Field.Index.UN_TOKENIZED ) );
return doc;
}
}

static class Company implements Source {
String name;
String addr;
String type;
Company( String name, String addr, String type ){
this.name = name;
this.addr = addr;
this.type = type;
}
public Document getDocument(){
Document doc = new Document();
doc.add( new Field( F_NAME, name, Field.Store.YES, Field.Index.TOKENIZED ) );
doc.add( new Field( F_ADDR, addr, Field.Store.YES, Field.Index.TOKENIZED ) );
doc.add( new Field( F_TYPE, type, Field.Store.YES, Field.Index.TOKENIZED ) );
return doc;
}
}

static class Partner implements Source {
String number;
String name;
String level;
Partner( String number, String name, String level ){
this.number = number;
this.name = name;
this.level = level;
}
public Document getDocument(){
Document doc = new Document();
doc.add( new Field( F_NUMBER, number, Field.Store.YES, Field.Index.UN_TOKENIZED ) );
doc.add( new Field( F_NAME, name, Field.Store.YES, Field.Index.TOKENIZED ) );
doc.add( new Field( F_LEVEL, level, Field.Store.YES, Field.Index.UN_TOKENIZED ) );
return doc;
}
}
}



しかし、残念ながらこのプログラムは希望したとおりには動かない。確認したところ、やはりTermDocsではなくTermEnumを使用しなければならないということであった。TermEnumはインデックスから指定したTerm「以降」をすべて列挙するEnumerator型の抽象クラスで、IndexReader.terms()にTermの引数を渡すことで取得できる。このとき、すべてのTermを列挙するためにTermのフィールド値として空文字列を指定する。

先ほどのサンプルプログラムをTermEnumを使うようにして書き換えると、次のようになる。TermEnumはインデックス内の指定したTerm「以降」をすべて列挙するので、取得したTermのフィールドが指定したものかどうかをチェックする必要がある:



//---------------------------------------------------
// TermEnumですべてのTermを列挙
//---------------------------------------------------
IndexReader reader = IndexReader.open( dir );
TermEnum termEnum = reader.terms( new Term( F_ADDR, "" ) );
for( Term term = null;
( term = termEnum.term() ) != null && term.field().equals( F_ADDR );
termEnum.next() ){
System.out.println( "term: " + term.text() );
}
reader.close();



そして以下が実行例である。



term: 中町
term: 丸の内
term: 区
term: 千代田
term: 千川
term: 平野
term: 新橋
term: 東京
term: 板橋
term: 江戸川
term: 港
term: 目黒
term: 足立
term: 都



当該フィールドのすべての単語が列挙されているのがわかる。これとTermDocsを組み合わせてループさせればTermを含む文書を列挙できるが、同じ文書を重複して列挙しない工夫がいるのと、やはりTermEnumを使ってすべてのTermを列挙してしまうループがあるという点で、当初目的である「『住所』というフィールドがある文書はすべてその値に関わらず検索したい」を達成するには、単純に文書の種別などを登録したフィールドを持たせる、というのが無難であろう。

話を最初の「一番上の引き出し」に戻すと、考え方を変えればこの領域は私の仕事場の整理空間上の「遊び」の役割を果たしているともいえるのではないか。もし一番上の引き出しがなかったら、これらの小物たちはどこに置かれるのだろう。きっと机の上やプリンタの上、フタを閉じられたノートPCの上などに置かれ、引き出しの中以上の混乱を引き起こすのに違いない。そう考えて一番上の引き出しの混乱状態を納得し、かくして片付けられないまま歳を越すのであった。
| 関口宏司 | プロとして恥ずかしくないLuceneの大原則 | 10:06 | comments(0) | trackbacks(0) |
イーストタワー 〜 Luceneとボクと、時々、Flex 〜
私はJR山手線大崎駅にある「ゲートシティ大崎 イーストタワー」に向かって歩いていた。そのビルにはアドビシステムズが入っていてそこでFlex 2のトレーニングを受講するためである。アドビシステムズといえばIllustratorやPhotoshop、PDFのAcrobatで有名な成功企業であり、現在も成長し続けている、外資系IT企業の雄と目されている会社である。

一方、私はどこかでも書いたが、転職した外資系企業がことごとく消滅するという憂き目にあっている、人呼んで外資系だめんずうぉーかーである。

現在はそんな生活に終止符を打ち、会社を立ち上げて社長になっている。しかしだめさ加減はあまり変わっていないような気がする。そうすると外資系だからだめだったというわけではなかったのか。

週刊誌のSPA!で連載中の「だめんず・うぉ〜か〜」を読むと、わかっていてもダメ男を渡り歩いてしまう女性たちが登場する。それと反対に、イイ男とだけつきあったり結婚する女性たちがいるようだ。これはもう本能のレベルで違うようである。同じことが外資系IT企業にも当てはまり、外資系だめんずうぉーかーの私の反対が、アドビ社長のギャレット・イルグさんである。ギャレットさんとはBEA Systemsのときだけ重なったことがある。同社は私にしては珍しく、唯一現存している会社なのだった。ギャレットさんは成功企業だけを渡り歩いている優良外資系うぉーかーであるが、そもそも米国人の彼を外資系うぉーかーと呼ぶのはいかがなものか。そんなことを考えながらエレベーターを19階で降りると、そこはワンフロアすべてがアドビであった。さすがである。大きいビルなので総合受付までエレベーターを降りて少し歩かなければならなかった。

思えば私も面接のときに気がつくべきであった。

私が最後に勤めた外資系企業、そこの入社面接に出かけたときのことである。エレベーターを降りると、そこはすぐに壁が迫っていた。とても狭かったのである。目の前にある受付用の電話に手を伸ばす前に、すぐ右隣にあるトイレに入って気持ちを落ち着けなければならなかったのを今でもかすかに覚えている。今から考えると、そこではいろいろなだめ信号が発せられていた。今はその会社の実体はもうないのだが、その当時の同僚が3人もアドビに移っているのはいったいどうしたことだろう。彼らにはぜひ優良企業でがんばって欲しいと願うばかりである。私はその沈みかけた船から出るのが遅れて、最後の一人の船長になってしまった。その後その小さいビルの賃貸契約が終わるのを待って小さいマンションを見つけて一人で引っ越しを行い(まさに当時は来る日も来る日も荷物の梱包の体力勝負で、外資系というよりは体育会系であった)、最終的にオフィスを閉めたものである。外資系だめんずうぉーかーの最期としてはまことに貴重な体験であった。




最近はそういうことで、Luceneの合間に時々Flexをいじっている。以下はLucene本でも掲載している郵便番号のインクリメンタルサーチのFlex版デモである(未インストールの方はFlash 9がインストールされる):

インクリメンタルサーチデモ(Flex版)
http://demo.rondhuit-search.com/etcisdemo/flex.html


こういったクールなUIがFlexでは実に簡単に作成できる。一方、以下はLucene本で紹介しているAjax版のインクリメンタルサーチのデモである:

インクリメンタルサーチデモ(Ajax版)
http://demo.rondhuit-search.com/etcisdemo/


両方とも事業所名から検索できるように、Lucene本からのバージョンに機能追加をしている。

Ajax版の方はJavaScriptでIMEの変換確定前の文字列が読み取れることを利用して、Googleサジェストドコイク?のようにIME入力中のひらがなの時点でもインクリメンタルサーチを行っている。

一方、Flexの方はIMEの変換確定前の文字列が読み取れないようなので、ひらがなでも漢字でもとにかく変換を確定してやらないと検索に行かない。両方のバージョンは見栄え以外でもそういった使い勝手の違いがある。

Flexの方はIMEEvent.IME_COMPOSITIONというイベントを使って変換確定時のイベントを拾うことができ、結果的に「日本」と変換された漢字を得るために入力された文字が「にほん」なのか「にっぽん」なのかを調べることができるようだが、変換実行前にこれらの情報を取ることは私がマニュアルなどで調べた限りはわからなかった。どなたかFlexでIME変換確定前の文字列の取得に成功した人がいれば連絡して欲しい。
| 関口宏司 | LuceneとRIA | 13:25 | comments(1) | trackbacks(1) |
+ Solrによるブログ内検索
+ PROFILE
     12
3456789
10111213141516
17181920212223
24252627282930
31      
<< December 2006 >>
+ 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