関口宏司のLuceneブログ

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

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

| スポンサードリンク | - | | - | - |
「コンプリート版」サンプル、ダウンロード開始
「Apache Lucene入門」の出版から半年が経ち、Luceneをはじめ同書のサンプルプログラムが使用している周辺ツールのバージョンがバージョンアップした関係で、本に書いてあるとおりのバージョンをそろえるのに手間がかかるようになって来た。そこで、サンプルをLucene 2.0対応にすると同時に必要ツールの大部分をZIPファイルに同梱した「コンプリート版」なるものを作成した。

以下よりダウンロードできる:
http://www.rondhuit.com/publicity.html

書籍出版時はLucene 1.9がLuceneの最新バージョンであり、同書もLucene 1.9を想定して書かれている。しかし現在はLucene 2.0となっており、Lucene 2.0対応のJapaneseAnalyzerが出てきたりして、その関係でサンプルコードの一部がコンパイルできないものが出てきた。

Lucene 2.0になったときにLucene 1.9でdeprecatedであったAPIのほとんどが削除されたため、サンプルコードに同梱していたLucene-jaのデモがコンパイルできなくなった。出版時のLucene-jaのバージョンはLucene 1.4.3対応版だったため、Lucene 1.4.3当時に問題なかったAPIをそのデモの中で数多く呼び出していた。出版時はLucene 1.9でありdeprecatedになった程度であったので、Lucene-jaのデモはコンパイル時に警告メッセージが出るだけでコンパイルは通っていた。しかしLucene 2.0になったときにAPIそのものが別のものに置き換えるよう強制されてしまったため、コンパイルエラーが出るようになった。

また、Lucene-jaが出版当時のLucene 1.4.3対応版からLucene 2.0対応版に変わった際に、DigitFilterのコンストラクタがpublicからprotectedに変わってしまったので、DigitFilterを使用したサンプルプログラムのコンパイルが通らなくなってしまった。そのため、「コンプリート版」からはDigitFilterを使用したサンプルプログラムは削除してある。

「コンプリート版」にはlibディレクトリに多くの周辺ツール(JARファイル)を含んでいるのでサイズが大きい(約10MB)。

上記ダウンロードページからはこれまでの「旧版」もいまだダウンロード可能である。こちらはLucene 1.9を使い、本のAppendix Aに書いてあるバージョンに必要ツールのバージョンをきちんと合わせ、本の手順どおりにインストールと設定を行えば必ず動作するので、必要であればこちらも試していただきたい。
| 関口宏司 | 書籍「Apache Lucene入門」 | 12:58 | comments(2) | trackbacks(0) |
独自QueryParserの作成(5)
アクションの記述

前回生成規則をコード化したものに、JavaCCのアクションを記述する。最初にqueryExpr()であるが、これは次のようであった:



Query queryExpr() :
{
}
{
defaultExpr() | <OP_NOT> defaultExpr()
}



「|」の前のdefaultExpr()を考える。defaultExpr()はqueryExpr()と同じくQueryオブジェクトを返すので、この場合はdefaultExpr()の戻り値をそのまま返せばよい。したがって、次のようになる:



Query queryExpr() :
{
Query result = null;
}
{
result = defaultExpr() { return result; }
| <OP_NOT> defaultExpr()
}



次に、「NOT A」のように検索式の先頭にNOT演算子がついた場合の扱いであるが、これはdefaultExpr()から返されたQueryオブジェクトの全体の否定とすればよい。ただし、BooleanQueryで否定の単項は許されないので、Lucene 1.9から追加されたMatchAllDocsQueryとのANDをとるようにする。結局、queryExpr()は次のようになる:



Query queryExpr() :
{
Token t = null;
Query result = null;
}
{
result = defaultExpr() { return result; }
| <OP_NOT> result = defaultExpr() {
BooleanQuery bq = new BooleanQuery();
bq.add( new MatchAllDocsQuery(), BooleanClause.Occur.MUST );
bq.add( result, BooleanClause.Occur.MUST_NOT );
return bq;
}
}



次にdefaultExpr()を考える。defaultExpr()の生成規則は次のようであった:



Query defaultExpr() :
{
}
{
notExpr() ( notExpr() )*
}



したがって、これにアクションを加えると、次のようになる:



Query defaultExpr() :
{
Query result = null;
BooleanQuery bq = new BooleanQuery();
}
{
result = notExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
(
result = notExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
)*
{
if( bq.getClauses().length == 1 )
return bq.getClauses()[0].getQuery();
return bq;
}
}



以下同様に、notExpr()、orExpr()、andExpr()のアクションを埋めていく。

次にphraseExpr()であるが、これはやや複雑で次のようであった:



Query phraseExpr() :
{
}
{
<WORD> | <QUOTE> (<WORD>)+ <QUOTE> | <LP> defaultExpr() <RP>
}



ここで<WORD>は前々回のようにアクションを記述すると、次のようになるであろう:



Query phraseExpr()
{
Token t = null;
Query query = null;
BooleanQuery bq = new BooleanQuery();
}
{
t = <WORD> {
query = new TermQuery( new Term( field, t.image ) );
bq.add( query, BooleanClause.Occur.MUST );
}
:
}



しかしこのようにしてしまうと、<WORD>と認識された文字列がすべてLuceneのTermオブジェクトなってしまう。これではたとえば「機能定義書」というような文字列の場合具合が悪い。(JapaneseAnalyzerを使っている場合は)「機能定義書」は「機能」「定義」「書」のような3つのTermオブジェクトからなるPhraseQueryになるためである。

認識された文字列がどのように分割されるかはAnalyzerによって異なる。AnalyzerはMyQueryParserの場合は(QueryParserと同様)コンストラクタで渡される。MyQueryParserではこれをメンバ変数のanalyzerで保持しているので、これを用いて<WORD>の文字列を分解し、PhraseQueryに変換するようにしなければならない。

結局、phraseExpr()は次のようになる:



Query phraseExpr() :
{
Token t = null;
Query result = null;
TokenStream stream = null;
List<String> tokens = new ArrayList<String>();
}
{
t = <WORD> {
stream = analyzer.tokenStream( field, new StringReader( t.image ) );
try{
for( org.apache.lucene.analysis.Token token = stream.next(); token != null;
token = stream.next() )
tokens.add( token.termText() );
}
catch( IOException e ){
throw new ParseException( e.getMessage() );
}
finally{
try{
stream.close();
}
catch( IOException ignored ){}
}
if( tokens.size() == 1 )
result = new TermQuery( new Term( field, tokens.get( 0 ) ) );
else if( tokens.size() > 1 ){
PhraseQuery pq = new PhraseQuery();
for( String token : tokens )
pq.add( new Term( field, token ) );
result = pq;
}
return result;
}
| <QUOTE>
(
t = <WORD> {
stream = analyzer.tokenStream( field, new StringReader( t.image ) );
try{
for( org.apache.lucene.analysis.Token token = stream.next(); token != null;
token = stream.next() )
tokens.add( token.termText() );
}
catch( IOException e ){
throw new ParseException( e.getMessage() );
}
finally{
try{
stream.close();
}
catch( IOException ignored ){}
}
}
)+
<QUOTE>
{
if( tokens.size() == 1 )
result = new TermQuery( new Term( field, tokens.get( 0 ) ) );
else if( tokens.size() > 1 ){
PhraseQuery pq = new PhraseQuery();
for( String token : tokens )
pq.add( new Term( field, token ) );
result = pq;
}
return result;
}
| <LP>
result = defaultExpr()
<RP>
{
return result;
}
}



検索禁止語の対応

次に検索禁止語に対応するよう、プログラムに機能を追加する。

検索禁止語はStringの配列でMyQueryParserのコンストラクタに渡すことにする。ただし、アプリケーションによっては検索禁止語を判定する機能自体不要のものもあるので、検索禁止語のリストを渡さないバージョンのコンストラクタも用意する。Stringの配列で渡された検索禁止語は、HashSetで保持する。



public MyQueryParser( String field, Analyzer analyzer ){
this( field, analyzer, null );
}

public MyQueryParser( String field, Analyzer analyzer, String[] words ){
this( new FastCharStream( new StringReader( "" ) ) );
this.field = field;
this.analyzer = analyzer;
if( words != null ){
bannedWords = new HashSet( words.length );
for( String word : words )
bannedWords.add( word );
}
else
bannedWords = new HashSet();
}



検索式内にトークンとして認識された文字列と検索禁止語のリストを比較して検索禁止語の場合はParseExceptionをスローする次のようなメソッドを作成する:



private void checkBannedWord( String word ) throws ParseException {
// throw ParseException when a banned word is found
if( bannedWords.contains( word ) )
throw new ParseException( "a banned word was found : " + word );
}



このメソッドは前述のphraseExpr()にて<WORD>が認識されたときに呼び出すようにする。

MyQueryParserの全リスト

以上説明したMyQueryParserの全リストを以下に示す(実際にはロンウイットでも使用しているSimpleQueryParserという名前のクラスである。オープンソースソフトウェアで、自由に使っていただいてかまわない):



/**
* Copyright 2006 RONDHUIT Co.,Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

options {
JDK_VERSION = "1.5";
STATIC=false;
JAVA_UNICODE_ESCAPE=true;
USER_CHAR_STREAM=true;
}
PARSER_BEGIN(SimpleQueryParser)
package com.rondhuit.lucene.queryParser;

import java.io.*;
import java.util.*;
import org.apache.lucene.analysis.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;

/**
*
* A simple QueryParser with banned words feature.
*
* @author SEKIGUCHI, Koji / RONDHUIT Co.,Ltd.
*
*/
public class SimpleQueryParser {

private String field;
private Analyzer analyzer;
private Set bannedWords;

public static void main(String args[]){

final String F = "field";
final String[] queries = {
"question",
"question telephone",
"apple AND orange",
"apple AND orange OR foo",
"apple AND orange OR melon NOT strawberry",
"apple AND (orange OR melon NOT strawberry)",
"apple NOT strawberry",
"NOT strawberry",
"¥"apple melon¥"",
"¥"apple melon¥" AND bar"
};
final String[] words = { "foo", "bar" };

SimpleQueryParser parser = new SimpleQueryParser( F, new SimpleAnalyzer(), words );
for( String query : queries ){
try{
Query q = parser.parse( query );
System.out.print( query + " => " );
if( q != null )
System.out.println( q.toString() );
else
System.out.println( "<null>" );
}
catch( ParseException e ){
System.out.print( query + " => " );
System.out.println( e.getMessage() );
}
}
}

/**
* A constructor used when banned words feature is unnecessary.
*
* @param field the field name to be queried.
* @param analyzer the analyzer which is used to find terms in the query text.
*/
public SimpleQueryParser( String field, Analyzer analyzer ){
this( field, analyzer, null );
}

/**
* A constructor used when banned words feature is necessary.
*
* @param field the field name to be queried.
* @param analyzer the analyzer which is used to find terms in the query text.
* @param words an array of banned words string
*/
public SimpleQueryParser( String field, Analyzer analyzer, String[] words ){
this( new FastCharStream( new StringReader( "" ) ) );
this.field = field;
this.analyzer = analyzer;
if( words != null ){
bannedWords = new HashSet( words.length );
for( String word : words )
bannedWords.add( word );
}
else
bannedWords = new HashSet();
}

/**
* Parses the argument query text and returns a query object.
*
* @param query the query string to be parsed.
* @return the query object.
* @throws ParseException if the parsing fails or query string includes one or more banned words
*/
public Query parse( String query ) throws ParseException {
ReInit( new FastCharStream( new StringReader( query ) ) );
return queryExpr();
}

private void checkBannedWord( String word ) throws ParseException {
// throw ParseException when a banned word is found
if( bannedWords.contains( word ) )
throw new ParseException( "a banned word was found : " + word );
}
}
PARSER_END(SimpleQueryParser)

SKIP :
{
" " // en quad
| "¥u3000" // double-byte space
| "¥r"
| "¥t"
| "¥n"
}

TOKEN [IGNORE_CASE] :
{
< OP_AND : "AND" >
| < OP_OR : "OR" >
| < OP_NOT : "NOT" >
}

TOKEN :
{
< QUOTE : "¥"" >
| < LP : "(" >
| < RP : ")" >
}

TOKEN :
{
< WORD : (~[" ", "¥u3000", "¥r", "¥t", "¥n", "¥"", "(", ")"])+ >
}

Query queryExpr() :
{
Token t = null;
Query result = null;
}
{
<OP_NOT> result = defaultExpr() {
BooleanQuery bq = new BooleanQuery();
bq.add( new MatchAllDocsQuery(), BooleanClause.Occur.MUST );
bq.add( result, BooleanClause.Occur.MUST_NOT );
return bq;
}
| result = defaultExpr() { return result; }
}

Query defaultExpr() :
{
Query result = null;
BooleanQuery bq = new BooleanQuery();
}
{
result = notExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
(
result = notExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
)*
{
if( bq.getClauses().length == 1 )
return bq.getClauses()[0].getQuery();
return bq;
}
}

Query notExpr() :
{
Query result = null;
BooleanQuery bq = new BooleanQuery();
}
{
result = orExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
(
<OP_NOT>
result = orExpr(){
bq.add( result, BooleanClause.Occur.MUST_NOT );
}
)*
{
if( bq.getClauses().length == 1 )
return bq.getClauses()[0].getQuery();
return bq;
}
}

Query orExpr() :
{
Query result = null;
BooleanQuery bq = new BooleanQuery();
}
{
result = andExpr(){
bq.add( result, BooleanClause.Occur.SHOULD );
}
(
<OP_OR>
result = andExpr(){
bq.add( result, BooleanClause.Occur.SHOULD );
}
)*
{
if( bq.getClauses().length == 1 )
return bq.getClauses()[0].getQuery();
return bq;
}
}

Query andExpr() :
{
Query result = null;
BooleanQuery bq = new BooleanQuery();
}
{
result = phraseExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
(
<OP_AND>
result = phraseExpr(){
bq.add( result, BooleanClause.Occur.MUST );
}
)*
{
if( bq.getClauses().length == 1 )
return bq.getClauses()[0].getQuery();
return bq;
}
}

Query phraseExpr() :
{
Token t = null;
Query result = null;
TokenStream stream = null;
List<String> tokens = new ArrayList<String>();
}
{
t = <WORD> {
checkBannedWord( t.image );
stream = analyzer.tokenStream( field, new StringReader( t.image ) );
try{
for( org.apache.lucene.analysis.Token token = stream.next(); token != null;
token = stream.next() )
tokens.add( token.termText() );
}
catch( IOException e ){
throw new ParseException( e.getMessage() );
}
finally{
try{
stream.close();
}
catch( IOException ignored ){}
}
if( tokens.size() == 1 )
result = new TermQuery( new Term( field, tokens.get( 0 ) ) );
else if( tokens.size() > 1 ){
PhraseQuery pq = new PhraseQuery();
for( String token : tokens )
pq.add( new Term( field, token ) );
result = pq;
}
return result;
}
| <QUOTE>
(
t = <WORD> {
checkBannedWord( t.image );
stream = analyzer.tokenStream( field, new StringReader( t.image ) );
try{
for( org.apache.lucene.analysis.Token token = stream.next(); token != null;
token = stream.next() )
tokens.add( token.termText() );
}
catch( IOException e ){
throw new ParseException( e.getMessage() );
}
finally{
try{
stream.close();
}
catch( IOException ignored ){}
}
}
)+
<QUOTE>
{
if( tokens.size() == 1 )
result = new TermQuery( new Term( field, tokens.get( 0 ) ) );
else if( tokens.size() > 1 ){
PhraseQuery pq = new PhraseQuery();
for( String token : tokens )
pq.add( new Term( field, token ) );
result = pq;
}
return result;
}
| <LP>
result = defaultExpr()
<RP>
{
return result;
}
}



使い方

使い方は、Lucene付属のQueryParserと同様、コンストラクタにフィールド名とAnalyzerを渡してインスタンスを取得し、parse()メソッドを呼び出す。

SimpleQueryParserはQueryParserに加えてコンストラクタの第3引数に検索禁止語のリストをStringの配列にして渡すことができる。

LuceneのQueryParserのparse()に「要件定義書 AND テスト仕様書 OR 機能仕様書 NOT ユーザマニュアル」という文字列を渡すと、返されるQueryは次のようになる:



+f:"要件 定義 書" f:"テスト 仕様 書" f:"機能 仕様 書" -f:"ユーザ マニュアル"



今回開発したSimpleQueryParserのparse()に同じ検索質問文字列を渡すと、返されるQueryは次のようになる:



+((+f:"要件 定義 書" +f:"テスト 仕様 書") f:"機能 仕様 書") -f:"ユーザ マニュアル"



上記のように、SimpleQueryParserでは正しくBooleanQueryを構築できており、AND/OR/NOTを組み合わせた検索を行った場合にLuceneのQueryParserよりも望ましい検索結果が得られる。
| 関口宏司 | Lucene自由自在 | 15:07 | comments(3) | trackbacks(0) |
独自QueryParserの作成(4)
検索式演算子の優先度

AND/OR/NOTの演算子を含む検索式を正しくBooleanQueryに展開するために、演算子の「結合規則」や「優先順位」を決める必要がある。MyQueryParserではこれを次の表のように定めることとする:

演算子結合規則優先度
AND=>
OR=>
NOT(二項)=>
(省略)=>


「結合規則」はすべて=>となっており、これは「左結合」を意味する。また演算子NOTは二項演算子なので「A NOT B」のように使用する場面に適用されるものであり、「NOT A」のようなNOTについてはのちほど考えることにする。また演算子を省略した場合は初回に決めたMyQueryParserの仕様どおりANDと解釈する。

生成規則を作成する

左結合の二項演算子の式を表現する生成規則は次のとおりである(前回紹介した「JavaCC - コンパイラ・コンパイラ for Java」などを参照):



leftExpr() ::= higherExpr() ( <OP> higherExpr() )*



また、被演算子の式を表現する生成規則は次のとおりである:



prim() ::= <WORD> | <LP> expr() <RP>



これらの生成規則を前出の表の優先順位に沿って組み合わせていく。

最初は被演算子の式を表現する生成規則を使ってphraseExpr()を作成する。これは次のようである:



phraseExpr() ::= <WORD> | <LP> defaultExpr() <RP>



次に、表の中でもっとも優先度の高いAND演算子の生成規則を作成する。これは左結合なのでleftExpr()を用いて次のようになる:



andExpr() ::= phraseExpr() ( <OP_AND> phraseExpr() )*



ここで、leftExpr()の右辺のhigherExpr()は優先順位が上位のphraseExpr()に置き換えられている。

次に、表の中で次に優先度の高いOR演算子の生成規則を作成すると、次のようになる:



orExpr() ::= andExpr() ( <OP_OR> andExpr() )*



以下同様にNOTと省略時の演算子の生成規則を作成する:



notExpr() ::= orExpr() ( <OP_NOT> orExpr() )*

defaultExpr() ::= notExpr() ( notExpr() )*



以上をまとめると、検索式の生成規則は全体で次のようになる:



phraseExpr() ::= <WORD> | <LP> defaultExpr() <RP>
andExpr() ::= phraseExpr() ( <OP_AND> phraseExpr() )*
orExpr() ::= andExpr() ( <OP_OR> andExpr() )*
notExpr() ::= orExpr() ( <OP_NOT> orExpr() )*
defaultExpr() ::= notExpr() ( notExpr() )*



""を認識する

次に「"(ダブルクオーテーション)」でくくられた検索質問語を認識するように修正を加える。""でくくられるものは、ひとつ以上の単語でフレーズとなることから、前出のphraseExpr()に追加して、次のようになる:



phraseExpr() ::= <WORD> | <QUOTE> (<WORD>)+ <QUOTE> | <LP> defaultExpr() <RP>



ここで、「"」は<QUOTE>という表現に置き換えている。

「NOT 検索質問語」をサポートする

次に「NOT A」というフォーマットの検索式をサポートするように修正を加える。

ここで使われるNOTは前出の二項演算子のNOTとは異なり単項演算子である。しかも、(数式の単項演算子"-"(マイナス)とは異なり)検索式の途中には出現せず、常に先頭にのみ使われる。ということは、検索式は「defaultExpr()」かあるいは「NOT defaultExpr()」であると考えてよい。結局、検索式queryExpr()の生成規則は、全体として次のようになる:



phraseExpr() ::= <WORD> | <QUOTE> (<WORD>)+ <QUOTE> | <LP> defaultExpr() <RP>
andExpr() ::= phraseExpr() ( <OP_AND> phraseExpr() )*
orExpr() ::= andExpr() ( <OP_OR> andExpr() )*
notExpr() ::= orExpr() ( <OP_NOT> orExpr() )*
defaultExpr() ::= notExpr() ( notExpr() )*
queryExpr() ::= defaultExpr() | <OP_NOT> defaultExpr()



生成規則のコード化

生成規則を(読みやすさを考えて)順序を逆転し、JavaCCのコードに置き換えると次のようになる:



Query queryExpr() :
{
}
{
defaultExpr() | <OP_NOT> defaultExpr()
}

Query defaultExpr() :
{
}
{
notExpr() ( notExpr() )*
}

Query notExpr() :
{
}
{
orExpr() ( <OP_NOT> orExpr() )*
}

Query orExpr() :
{
}
{
andExpr() ( <OP_OR> andExpr() )*
}

Query andExpr() :
{
}
{
phraseExpr() ( <OP_AND> phraseExpr() )*
}

Query phraseExpr() :
{
}
{
<WORD> | <QUOTE> (<WORD>)+ <QUOTE> | <LP> defaultExpr() <RP>
}



次回は上記JavaCCのコードにアクションを記述する。また、検索禁止語への対応は(JavaCCではなく)Javaのプログラムでおこなうため、アクションを記述したあとに考える。
| 関口宏司 | Lucene自由自在 | 07:23 | comments(4) | trackbacks(1) |
書籍に関する問い合わせフォームの設置
書籍「Apache Lucene入門」と「Apache Ant」の「お問い合わせフォーム」を設置したので、不明点・不具合・ご要望・ご質問などお知らせください。

なお、個別の質問に回答するというよりは、FAQなどを作成するのに利用しようと思っているので、あらかじめご了承ください。

書籍に関する問い合わせフォーム
http://www.rondhuit.com/publicity.html
| 関口宏司 | 書籍「Apache Lucene入門」 | 09:29 | comments(0) | trackbacks(0) |
Amazonの読者評価・・・星ひとつ
先日、久しぶりにAmazonをチェックしたら「Apache Lucene入門」にはじめてのレビューの書き込みが!・・・と思ったらナント星がひとつ。。。がーん。

「いただきました、星ひとつですっ!」

この瞬間ほど堺正章の悲哀を感じたことはない。しかし堺シェフは毎週その痛みに耐えているのである。私も真摯に評価者の声に耳を傾けてみよう。


「4章までは理解しやすくGOOD」とのこと。ありがとうございます。

しかし、「サンプルがビルドできない」とご立腹のご様子。そうでしたか。私の周りではそのような声はないので、気がつかないビルド手順の説明の抜けがあったのかもしれません。申し訳ございませんでした。私やいつも一緒に仕事をしている人はビルドに使用しているAntを使いこなしているので「入門書」にしてはLucene以外の部分で難しかったかもしれません。


・・・ということで周囲の声を聞いているだけではわからない・気がつかない点があるようなので、近々問い合わせページを開設し、可能な範囲で対応できるようにしようと思っている。しばらくお待ちいただきたい。
| 関口宏司 | 書籍「Apache Lucene入門」 | 09:57 | comments(2) | trackbacks(0) |
独自QueryParserの作成(3)
構文ダイアグラム

今回は簡単な検索質問の文字列を解析するMyQueryParserを開発する手順を解説する。

このMyQueryParserはスペース(タブや全角スペースも含む)で区切られた単語からTermQueryを生成し、BooleanQueryで全体を接続する。AND/OR/NOTの演算子も認識し、省略された場合はANDと解釈する。

次の表は検索質問とMyQueryParserでの解析結果の例である。解析結果はQuery.toString()を示している(フィールド名を"f"とする):

検索質問文字列解析結果
検索 質問 電話+f:検索 +f:質問 +f:電話
要件定義書 AND テスト仕様書+f:要件定義書 +f:テスト仕様書
要件定義書 and テスト仕様書 or 機能仕様書 NOT ユーザマニュアル+f:要件定義書 +f:テスト仕様書 f:機能仕様書 -f:ユーザマニュアル


このような検索質問入力の文法を「構文ダイアグラム」という図で表すと、以下のようになる。

構文ダイアグラム

この構文ダイアグラムはひとつの「生成規則」からなり、生成規則の名前は「検索式」である。図を見るとわかるとおり、検索式(検索質問)はひとつ以上の単語からなり、任意の回数繰り返して記述される。そしてその単語と単語の間に"AND"や"OR"や"NOT"などを記述してもよく、これらの演算子を省略してもよい。・・・とまあ、こういった文法が構文ダイアグラムで表すとわかりやすい。

MyQueryParser.jj

MyQueryParser.jjのプログラムを以下に示す。



options {
JDK_VERSION = "1.5";
STATIC=false;
JAVA_UNICODE_ESCAPE=true;
USER_CHAR_STREAM=true;
}
PARSER_BEGIN(MyQueryParser)
package queryParser.simple;

import java.io.*;
import java.util.*;
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;

public class MyQueryParser {

private String field;
private Analyzer analyzer;

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

final String F = "field";
final String[] queries = {
"検索 質問 電話",
"要件定義書 AND テスト仕様書",
"要件定義書 and テスト仕様書 or 機能仕様書 NOT ユーザマニュアル"
};

MyQueryParser parser = new MyQueryParser( F, new JapaneseAnalyzer() );
for( String query : queries ){
Query q = parser.parse( query );
System.out.print( query + " => " );
if( q != null )
System.out.println( q.toString() );
else
System.out.println( "<null>" );
}
}

public MyQueryParser( String field, Analyzer analyzer ){
this( new FastCharStream( new StringReader( "" ) ) );
this.field = field;
this.analyzer = analyzer;
}

public Query parse( String query ) throws ParseException {
ReInit( new FastCharStream( new StringReader( query ) ) );
return queryExpr();
}
}
PARSER_END(MyQueryParser)

SKIP :
{
" " // 半角スペース
| " " // 全角スペース
| "¥r"
| "¥t"
| "¥n"
}

TOKEN [IGNORE_CASE] :
{
< OP_AND : "AND" >
| < OP_OR : "OR" >
| < OP_NOT : "NOT" >
}

TOKEN :
{
< WORD : (~[" ", " ", "¥r", "¥t", "¥n", "¥"", "(", ")"])+ >
}

Query queryExpr() :
{
Token t = null;
Token op = null;
Query query = null;
BooleanQuery bq = new BooleanQuery();
}
{
t = <WORD> {
query = new TermQuery( new Term( field, t.image ) );
bq.add( query, BooleanClause.Occur.MUST );
}
(
( op = <OP_AND> | op = <OP_OR> | op = <OP_NOT> )?
t = <WORD> {
query = new TermQuery( new Term( field, t.image ) );
if( op == null )
bq.add( query, BooleanClause.Occur.MUST );
else if( op.image.equalsIgnoreCase( "AND" ) )
bq.add( query, BooleanClause.Occur.MUST );
else if( op.image.equalsIgnoreCase( "OR" ) )
bq.add( query, BooleanClause.Occur.SHOULD );
else
bq.add( query, BooleanClause.Occur.MUST_NOT );
}
)*
{
return bq.getClauses().length == 0 ? null : bq;
}
}



上記のプログラムを適当なブロックに分けて、以下で説明する。

optionsの指定

JavaCCコンパイル時にコマンドラインで指定できるオプションを、このように.jjファイルに埋め込むことができる。オプションには多数のプロパティがあるので、詳しくはhttps://javacc.dev.java.net/doc/javaccgrm.html#prod2を参照していただきたい。MyQueryParser.jjで指定しているものはJDK_VERSIONを除き、QueryParserで指定しているものをコピーした。

構文解析プログラム本体部分

構文解析プログラム本体の部分をPARSER_BEGINとPARSER_ENDの間に記述する。書き方は次のように.jjファイルのファイル名と括弧の中とクラス名をすべて同じ名前にする。



PARSER_BEGIN(MyQueryParser)
package queryParser.simple;

import java.io.*;
:

public class MyQueryParser {
:
}
PARSER_END(MyQueryParser)



MyQueryParser.jjではPARSER_BEGINとPARSER_ENDの間にコンストラクタとparse()メソッドを定義しているが、このように生成規則以外のメソッド(後述)が必要な場合は、PARSER_BEGINとPARSER_ENDの間に記述する。

MyQueryParserのコンストラクタはQueryParserのコンストラクタと同じ使い方ができるよう、第一引数にフィールド名を、第二引数にAnalyzerを渡すようになっている。parse()メソッドも同様にQueryParserと同じインタフェースにしており、引数に検索質問語の文字列を受け取り、Queryオブジェクトを返すようになっている。また、ParseExceptionをスローするところも同じである。

また、MyQueryParserにはmain()メソッドを持たせているが、これはMyQueryParser単独でテストができるようにしたサンプルクライアントである。

FastCharStreamクラスの流用

MyQueryParserではLuceneのQueryParserのために実装されたFastCharStreamクラスをコピーして使用している。このクラスは前述のoptionsでUSER_CHAR_STREAMにtrueと指定したときに生成されるCharStreamインタフェースのLucene用の効率的な実装クラスである。

MyQueryParserではFastCharStream.javaをorg.apache.lucene.queryParserパッケージからコピーし、パッケージをqueryParser.simpleに変更して流用している。

SKIPの指定

SKIPでは、字句解析ルーチンが入力から読み飛ばす文字を指定する。MyQueryParser.jjでは次のように半角・全角スペース、復帰・改行およびタブをスキップしている:



SKIP :
{
" " // 半角スペース
| " " // 全角スペース
| "¥r"
| "¥t"
| "¥n"
}



TOKENの指定

字句解析ルーチンがトークンとして切り出す文字列をTOKENのブロックで次のように指定する:



TOKEN :
{
< WORD : (~[" ", " ", "¥r", "¥t", "¥n", "¥"", "(", ")"])+ >
}



トークンの定義は「<名前:パターン>」という具合に記述する。パターンは正規表現で記述するようになっており、上記の場合は「半角・全角スペース、タブ、復帰・改行、・・・を除く文字の1つ以上の繰り返し」をWORDというトークン名に指定している。
以下のように「IGNORE_CASE」を指定すると、アルファベットの大文字小文字が区別されなくなる:



TOKEN [IGNORE_CASE] :
{
< OP_AND : "AND" >
| < OP_OR : "OR" >
| < OP_NOT : "NOT" >
}



したがって、「AND」でも「and」でも「And」でもOP_ANDというトークンとみなされる。

生成規則のコード化

最後に生成規則をJavaCCのプログラムコードにおとす。MyQueryParserの生成規則「検索式」は既に「構文ダイアグラム」で図示したが、BNF(Backus-Naur Form)という記述の仕方で書き表すと次のようになる:



queryExpr() ::= <WORD> ( ( <OP_AND> | <OP_OR> | <OP_NOT> )? <WORD> )*



ここでqueryExpr()は「検索式」と考えていただきたい。また、<WORD>は単語、<OP_AND>、<OP_OR>、<OP_NOT>はそれぞれAND、OR、NOTの演算子である。さらに"|"はOR(または)の意味で、()?はカッコ内が省略可能であること、()*はカッコ内の0回以上の繰り返しを表す(ここには出てこないが、()+で1回以上の繰り返しの意味になる)。上記のBNFと前述の構文ダイアグラムがまったく同じであることがわかるだろう。

このBNFの式を次のようにJavaCCの記述に合うように書き直す:



Query queryExpr() :
{
}
{
<WORD> ( ( <OP_AND> | <OP_OR> | <OP_NOT> )? <WORD> )*
}



BNFをJavaCCのコードに書き直したときは、上記のように左辺(非終端記号とも呼ばれる)に戻り値のタイプ(上記ではQuery)を記述する。戻り値がないときはvoidとする。

次に、上記のコードに必要な「アクション」を書き足していくが、アクションの中でトークンが参照できるよう、次のようにToken変数に代入する:



Query queryExpr() :
{
Token t = null;
Token op = null;
}
{
t = <WORD> ( ( op = <OP_AND> | op = <OP_OR> | op = <OP_NOT> )? t = <WORD> )*
}



TokenクラスはJavaCCが生成するクラスであってLuceneのTokenではないので注意していただきたい。上記のように最初の{}で囲まれた宣言部でToken変数を宣言する(こうするとqueryExpr()メソッドのローカル変数になる)。

アクションの記述

「アクション」はトークンなどの生成規則の右辺の構成要素が正しく認識できたときに実行されるJavaのコードであり、構成要素(トークンなど)の後に{}でくくって記述する。

たとえば、最初の単語(<WORD>)が認識された後にその単語からTermQueryオブジェクトを作成してBooleanQueryに追加するには、次のようになる:



Query queryExpr() :
{
Token t = null;
Token op = null;
Query query = null;
BooleanQuery bq = new BooleanQuery();
}
{
t = <WORD> {
query = new TermQuery( new Term( field, t.image ) );
bq.add( query, BooleanClause.Occur.MUST );
}
( ( op = <OP_AND> | op = <OP_OR> | op = <OP_NOT> )? t = <WORD> )*
}



ここで「t.image」はTokenクラスのimage属性でトークンの文字列(String)を表す。

同じように、2番目以降の<WORD>が認識されたときと、右辺の構成要素がすべて認識され終わったときに実行されるアクションを加えて、最終的に次のようなコードを得る:



Query queryExpr() :
{
Token t = null;
Token op = null;
Query query = null;
BooleanQuery bq = new BooleanQuery();
}
{
t = <WORD> {
// 最初の<WORD>が見つかったときに実行されるアクション
query = new TermQuery( new Term( field, t.image ) );
bq.add( query, BooleanClause.Occur.MUST );
}
(
( op = <OP_AND> | op = <OP_OR> | op = <OP_NOT> )?
t = <WORD> {
// 2番目以降の<WORD>が見つかったときに実行されるアクション
query = new TermQuery( new Term( field, t.image ) );
if( op == null )
bq.add( query, BooleanClause.Occur.MUST );
else if( op.image.equalsIgnoreCase( "AND" ) )
bq.add( query, BooleanClause.Occur.MUST );
else if( op.image.equalsIgnoreCase( "OR" ) )
bq.add( query, BooleanClause.Occur.SHOULD );
else
bq.add( query, BooleanClause.Occur.MUST_NOT );
}
)*
{
// 最後に実行されるアクション
return bq.getClauses().length == 0 ? null : bq;
}
}



MyQueryParserの実行

既に紹介した開発環境がある場合、次のようにAntを使ってMyQueryParserのmain()メソッドを実行できる:



$ ant -emacs run -Dp=queryParser.simple.MyQueryParser
Buildfile: build.xml

javacc:
Java Compiler Compiler Version 4.0 (Parser Generator)
(type "javacc" with no arguments for help)
Reading from file C:¥Project¥blog¥lucene¥src¥queryParser¥simple¥MyQueryParser.jj
. . .
File "TokenMgrError.java" does not exist. Will create one.
File "ParseException.java" does not exist. Will create one.
File "Token.java" does not exist. Will create one.
File "CharStream.java" does not exist. Will create one.
Parser generated successfully.

compile:
Compiling 7 source files to C:¥Project¥blog¥lucene¥classes

run:
検索 質問 電話 => +field:検索 +field:質問 +field:電話
要件定義書 AND テスト仕様書 => +field:要件定義書 +field:テスト仕様書
要件定義書 and テスト仕様書 or 機能仕様書 NOT ユーザマニュアル => +field:要件定
義書 +field:テスト仕様書 field:機能仕様書 -field:ユーザマニュアル

BUILD SUCCESSFUL
Total time: 3 seconds



今回はJavaCCの解説も兼ねたのでMyQueryParserは簡単な機能を持たせただけだったが、次回以降で当初の仕様を満たすMyQueryParserを作成する。

なお、この記事を書くにあたっては以下のJavaCCの解説書を参考にした。

JavaCC - コンパイラ・コンパイラ for Java
五月女 健治 (著) テクノプレス
http://www.amazon.co.jp/exec/obidos/ASIN/4924998648/
| 関口宏司 | Lucene自由自在 | 08:59 | comments(0) | trackbacks(0) |
+ Solrによるブログ内検索
+ PROFILE
   1234
567891011
12131415161718
19202122232425
2627282930  
<< November 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