関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
QueryParserでMatchAllDocsQueryを使う
少し古い話になるが、Lucene 2.1からQueryParserに"*:*"という文字列を指定すると、MatchAllDocsQueryに翻訳してくれる機能が追加された:

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

MatchAllDocsQueryはインデックス中の全ドキュメントを取得するためのQueryである。そのものずばり「インデックス中の全ドキュメントを取得する」目的で使用できるのはもちろんのこと、MUST_NOTと共に使用して「指定した単語やフレーズを含まない全ドキュメントを取得する」のにも使用できる(Lucene本 P.194-195)。

"*:*"がMatchAllDocsQueryに解釈される様子は、次のプログラムで簡単に確認できる:



QueryParser qp = new QueryParser( F, analyzer );
Query query = qp.parse( "*:*" );
System.out.println( "*:* => " + query.toString() );



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



*:* => MatchAllDocsQuery



「安倍晋三」を含まないすべてのドキュメントを得るためのQueryを取得するには、次のようにすればよい:



Query query = qp.parse( "*:* NOT 安倍晋三" );


| 関口宏司 | プロとして恥ずかしくないLuceneの大原則 | 17:29 | comments(0) | trackbacks(0) |
お手軽なメモリ節約法
ドキュメントを構成するあるインデックスフィールドのnorm factorが不要な場合、当該フィールドに対してsetOmitNorms(true)を指定すると、検索時のヒープ消費量が最大で「ドキュメント数分のバイト」だけ節約できる。

たとえば、インデックスに100万件のドキュメントがあるとき、1つのインデックスフィールドに対してsetOmitNorms(true)を実行すると、そのフィールドを検索するプログラムではsetOmitNorms(true)を実行しない場合と比べて1Mバイト(ここではMは百万)のメモリが節約できる。これはLuceneでは検索のたびにnorm factorをインデックス(.nrmファイルに相当)から読むのではなく、一度読んだnorm factorをメモリ上にキャッシュするためである。

このキャッシュはフィールド名をキーにしたテーブルで管理されているため、setOmitNorms(true)を行ったフィールドの分だけ節約の効果が出てくる。たとえば、5つのインデックスフィールドでsetOmitNorms(true)を実行すると、100万件の文書では5Mバイト、1,000万件の文書では50Mバイトのヒープが節約できる。

文書数が大きなインデックスほど節約効果が大きく簡単に試せるため、覚えておくとよいだろう。
| 関口宏司 | プロとして恥ずかしくないLuceneの大原則 | 22:53 | comments(0) | trackbacks(0) |
IndexWriterの新しいコンストラクタ
世の中、Web2.0が花盛りである。

何を隠そう、弊社もWeb2.0企業へ変身しようと社内改革に真剣に取り組んでいる。そのためにまず、Web2.0とは何かを調べる必要がある。Web2.0の日本語のバイブル的取り上げ方をされる「ウェブ進化論」は少し前に読んだが、最近立て続けに次のような関連本を読んだ:



どれも面白くためになった。どうやらWeb2.0のための7つの原則とかいうものがあるらしく、それは次のようなものである:


  1. プラットフォームとしてのウェブ

  2. 集合知の利用

  3. データは次世代の「インテル・インサイド」

  4. ソフトウェア・リリースサイクルの終焉

  5. 軽量なプログラミングモデル

  6. 単一デバイスの枠を超えたソフトウェア

  7. リッチなユーザー経験



しかしどうもよくわからない。Web2.0企業になるためには何をすればいいのだろう。

そこで思いついたのは、これらの本で必ず取り上げられているWeb2.0企業の最先端、Googleのまねをすることである。Googleのまねをするというと人は「検索エンジンの開発でもするのか」と想像するであろう。そうではない。私が開発したわけではないが、当社は既に検索エンジンを持っていてそれで商売をしている。ここはもっと意表をつくやり方でやってみたい。たとえばこういうのはどうだろう。

私の昔の同僚でGoogleに移籍した者がおり、彼と飲んだときのことである。Googleについて彼が熱く語ったいくつかの逸話のうち、ひどく私の印象に残った話は「Googleは社員に無料で食事を振舞う」というものであった。これは今でこそ広く知れ渡っているGoogleの福利厚生のひとつであるが、当時は驚きと感心をもって聞いたものである。さすがGoogleだ。無料サービスがこんなところでも行われているのだった。まずはこれを取り入れさせてもらうことにする。

しかし、ただまねするだけではだめだ。Googleの上を行くために、うちは3食すべて会社で払うことにする。そうなると3食すべて外食にしなければならない(社員食堂がまだないので)。これはなかなか大変なことだが、Web2.0のためには乗り越えなければならない壁である。そして領収書を受け取り、申告にあたってはきちんと残らず添付する。3食すべて領収書があるので、その分量といったら大変な枚数になるだろう。毎月の領収書の糊付けの作業だけで一日費やすことになってしまうかもしれない。しかし、Web2.0のためなので文句は言えない。すると税務署は大量の食費の領収書を見てあきれ、こういうだろう。

「この領収書は認められません」

「いえ、問題ないはずです」

「どうしてですか」

「当社はWeb2.0企業ですから」

税務署はとまどうばかりだ。

しかし、税金を取るために執念を燃やす彼らは、署に戻りWeb2.0に関する勉強会を開いて対策を立てて出直すだろう。そして彼らはまたもこう指摘するのだった。

「これは認められません」

「どうしてですか」

「この領収書は1.0ですから」

なんと、いつの間にか税務署も2.0化しているのだった。

2.0の前で人は無力である。




2.0といえば、Luceneのバージョンも今2.0である。

次のバージョンとされている2.1は残Issueの数だけで見ればもうすぐ出そうな気配である。バグフィックスだけでなく、パフォーマンスについても前回書いたとおり改善されている。そのほかの改善点でひとつのわかりやすい例をあげると、IndexWriterのコンストラクタの追加がある。

IndexWriterの現在のコンストラクタは、3つの引数を取り、3番目の引数のboolean createの扱いがやっかいなものであった。この引数をtrueにすると、1番目の引数で指定したインデックスを作成するのであるが、インデックスがあってもなくても新規作成してしまう。逆にfalseにするとインデックスがないときはFileNotFoundExceptionをスローする。わかりやすく表にすると、次のようになる:

インデックスありインデックスなし
create=true新規作成新規作成
create=false追加FileNotFoundException


この表で、赤字にしたところがやっかいな動作の部分だ。Lucene本でもこの部分については数ページを割き、最終的にIndexReader.indexExists()メソッドを使うことを提案している。

やはりこのフラグは多くの人を戸惑わせていたようで改善提案がなされた。オリジナルの提案では、3番目の引数をcreateではなくclearと改名し、clear=falseのときでインデックスがないときはFileNotFoundExceptionではなく新規作成するようにして欲しい、というものであった。こうすればエンドユーザには影響がないであろう、という主旨のようだが、これでは既存のプログラムが影響を受けるのでこの案は受け入れられなかったが、その代わり新しいコンストラクタが導入されることになった。この新しいコンストラクタでは、3番目の引数がはぶかれている。そして動作としては上記の表の黒字部分のようになる。つまりインデックスがあれば追加し、なければ新規作成する、というコンストラクタである。

これはおそらく今後最も使われるIndexWriterのコンストラクタになると私は予想している。
| 関口宏司 | プロとして恥ずかしくないLuceneの大原則 | 10:04 | comments(4) | trackbacks(1) |
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の大原則
最近、「プロとして恥ずかしくない何々の大原則」という本が売れているようである。「何々」には、「Illustrator」や「Photoshop」、「スタイルシート」などが入る。

ためしにamazonの検索に「プロとして恥ずかしくない」と入れると、ずらっと出てくる。どうやらシリーズ化しているようだ。

「リストマニア!」という欄には「プロ恥シリーズ」というリンクまでできていた。「プロ恥」という略語ができるくらい流行っているのか?

しかし、「プロ」と「恥」という、本来およそ隣り合ってはいけない用語が連続して登場するタイトルの本が売れているとは、どういうことだろう。

彼らはその道のプロとして仕事をしている。家庭や会社の人間関係に世間並みの悩みもあるが、なんとか日々の生活を営んでいる。仕事もそれなりに忙しい。2006年の日本において高望みさえしなければ、「これでよしとしようか」そんな感情なのであった。しかしそんなある日、仕事と仕事の谷間にちょっとした不安感が彼らを襲うのであった。

「先日納品したあの仕事、プロとして恥ずかしかったかもしれない」。

しかし彼らは、プロである。そしてプロは孤独だ。かくして誰にも相談できずにまた日々の忙しさに巻き込まれ、「プロ恥」の不安感は彼の心の奥深くにしまいこまれてしまうのであった。

以上は私の想像であるが、「プロ恥」シリーズが売れているということは、実体もそれほど違わないのかもしれない。

「プロ恥」シリーズが「Illustrator」や「Photoshop」だからまだいい。「医者」だったらどうか。

「プロとして恥ずかしくない医者の大原則」

そんな医者はいやだ。絶対かかりたくないであろう。

体調が悪くなって、近所の診療所に行ったとする。そこは子供のころから何かあれば通っている、いわば「かかりつけ」の病院だ。久しぶりに行くと、子供のころに診てくれていたおじいさん先生ではなかった。聞いてみるとその先生はおじいさん先生の息子、つまり2代目だ。診療所内もいまどきの病院らしく、小さいながらもシックなインテリアで統一・改装され、待合室にはUSENが静かに流れているのであった。そういえば、おじいさん先生のころに流れていたのはラジオ番組「子供電話相談室」だったことを考えると、ずいぶんな進歩である。

そしていよいよ自分の名前が呼ばれ、診察室に入った。「今日はどうされましたか?」と2代目先生。先生に症状を伝えようとしたその瞬間、彼の机上に一冊の本が置いてあるのが目に入ってしまった。それは「プロとして恥ずかしくない医者の大原則」というタイトルで、ページのところどころから黄色や水色のポストイットがはみ出ている。

こんな医者はやめておいたほうがよい。




ということで、流行の「プロ恥」に私も便乗することにして「プロとして恥ずかしくないLuceneの大原則」というのを考えてみることにした。

今日は(も)無駄話に時間がかかってしまったので(これから出かけなければいけないのだ)、簡単なテーマを取り上げることにする。「プロとして恥ずかしくないLucene」はすぐには思いつかないので、逆に「プロとして恥ずかしいLuceneの使い方」を示すことにした。つまりこれを見習わないことで恥をかかなくてすむ、というわけである。

これは最近だったか、Luceneのメーリングリストに流れていたものだ。あるプログラムがドキュメントの数だけループして、次のコードを呼び出してインデックスに索引付けしている。しかしながら、いざ検索すると、含まれているはずのキーワードで検索できない。「なぜ検索できないのでしょう、助けてください」というものであった:



protected void addOrUpdate(Document doc) throws IOException {
IndexWriter indexWriter = null;
Analyzer analyzer = new StandardAnalyzer();
try {
indexWriter = new IndexWriter(directory, analyzer, true);
indexWriter.addDocument(doc);
} finally {
indexWriter.close();
}
}



答えは、IndexWriterのコンストラクタを呼ぶ際の第3パラメータにtrueを渡しているから、である。このようにしてしまうと、第一パラメータのdirectoryで指定したインデックスを毎回新規作成してしまうので、結果としてループの最後に追加したドキュメントしか索引付けされない状態になってしまうのだ。
| 関口宏司 | プロとして恥ずかしくないLuceneの大原則 | 10:11 | comments(2) | trackbacks(1) |
+ Solrによるブログ内検索
+ PROFILE
   1234
567891011
12131415161718
19202122232425
262728293031 
<< August 2018 >>
+ 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