関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
<< 独自QueryParserの作成(4) | main | 「コンプリート版」サンプル、ダウンロード開始 >>
スポンサーサイト

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

| スポンサードリンク | - | | - | - |
独自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) |
スポンサーサイト
| スポンサードリンク | - | 15:07 | - | - |
はじめまして。いつも参考にさせて貰っています。

独自のQueryParserに興味があり、Eclipseを使ってJavaCCの環境構築を行い、SimpleQueryParserをコンパイル、実行しました。そこで質問が2つあるのです。

(1)StandardAnalyzerを指定して、"[test]"という文字列を入力すると、tokenStreamでトークンを区切った後に、"test"というように括弧が無くなっていました。[]、()、{}などを普通にトークンの一部として扱うにはどうすればよいでしょうか?

(2)"文字列1 文字列2"のように2つの単語をスペースを挟んでAND検索する場合、BooleanQueryでBooleanClause.Occur.MUSTを指定して結合するのは理解したのですが、この場合だと"and","or","not"の文字列自体がトークンに指定できません。これらをトークンに指定するにはどうすればよいでしょうか?

長くなってしまいましたが、ヒントが頂ければと思います。よろしくお願いいたします。
| yosi | 2007/11/08 9:20 PM |
(1)は、()についてはSimpleQueryParserの仕様で検索式の括弧とみなすとしているのでSimpleQueryParserの仕様を変えるなどの措置が必要です。[]{}についてはStandardAnalyzerにて捨てているようですので、StandardAnalyzerを変更するか別のAnalyzerを作成するなどが必要となります。

(2)は、AND/OR/NOT(大文字)をSimpleQueryParserの仕様で検索式の演算子とみなしているのでSimpleQueryParserの仕様を変えるなどの措置が必要です。and/or/not(小文字)が落とされるのはStandardAnalyzerのストップワードになっているためでしょう。ストップワードについてはLucene本に書いてあるので参考にしてください。
| 関口 | 2007/11/09 10:12 AM |
即答ありがとうございます。

(1)については、NGramTokenizerを使ったtokenStreamで独自のAnalyzerを作成してみました。併せてQueryParser#escapse()で特殊記号のエスケープをして対処しました。これでよいのか謎ですが…。

(2)については、tokenStreamでStopFilterを処理しないようにしました。

それにしてもユーザからの入力を汎用的に処理するのは意外と大変ですね。
これからも本とブログ共に参考にさせて頂きます。
| yosi | 2007/11/10 10:12 AM |









トラックバック機能は終了しました。
+ Solrによるブログ内検索
+ PROFILE
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
<< August 2020 >>
+ 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