3.1、聲明QueryParser類
在QueryParser.jj文件中,PARSER_BEGIN(QueryParser)和PARSER_END(QueryParser)之間,定義了QueryParser類。
其中最重要的一個函數(shù)是public Query parse(String query)函數(shù),也即我們解析Lucene查詢語法的時候調(diào)用的函數(shù)。
這是一個純Java代碼定義的函數(shù),會直接拷貝到QueryParser.java文件中。
parse函數(shù)中,最重要的一行代碼是調(diào)用Query res = TopLevelQuery(field),而TopLevelQuery函數(shù)是QueryParser.jj中定義的語法分析器被JavaCC編譯后會生成的函數(shù)。
3.2、聲明詞法分析器
在解析詞法分析器之前,首先介紹一下JavaCC的詞法狀態(tài)的概念(lexical state)。
有可能存在如下的情況,在不同的情況下,要求的詞法詞法規(guī)則不同,比如我們要解析一個java文件(即滿足java語法的表達式),在默認的狀態(tài) DEFAULT下,是要求解析的對象(即表達式)滿足java語言的詞法規(guī)則,然而當(dāng)出現(xiàn)"/**"的時候,其后面的表達式則不需要滿足java語言的語 法規(guī)則,而是應(yīng)該滿足java注釋的語法規(guī)則(要識別@param變量等),于是我們做如下定義:
//默認處于DEFAULT狀態(tài),當(dāng)遇到/**的時候,轉(zhuǎn)換為IN_JAVADOC_COMMENT狀態(tài) <DEFAULT> TOKEN : {<STARTDOC : “/**” > : IN_JAVADOC_COMMENT } //在IN_JAVADOC_COMMENT狀態(tài)下,需要識別@param變量 <IN_JAVADOC_COMMENT> TOKEN : {<PARAM : "@param" >} //在IN_JAVADOC_COMMENT狀態(tài)下,遇到*/的時候,裝換為DEFAULT狀態(tài) <IN_JAVADOC_COMMENT> TOKEN : {<ENDDOC: "*/">: DEFAULT } |
<*> 表示應(yīng)用于任何狀態(tài)。
(1) 應(yīng)用于所有狀態(tài)的變量
<*> TOKEN : { <#_NUM_CHAR: ["0"-"9"] > //數(shù)字 | <#_ESCAPED_CHAR: "\\" ~[] > //"\"后的任何一個字符都是被轉(zhuǎn)義的 | <#_TERM_START_CHAR: ( ~[ " ", "\t", "\n", "\r", "\u3000", "+", "-", "!", "(", ")", ":", "^", "[", "]", "\"", "{", "}", "~", "*", "?", "\\" ] | <_ESCAPED_CHAR> ) > //表達式中任何一個term,都不能以[]括起來的列表中的lucene查詢語法關(guān)鍵字開頭,當(dāng)然被轉(zhuǎn)義的除外。 | <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) > //表達式中的term非起始字符,可以包含任何非語法關(guān)鍵字字符,轉(zhuǎn)義過的字符,也可以包含+, -(但包含+,-的符合詞法,不合語法)。 | <#_WHITESPACE: ( " " | "\t" | "\n" | "\r" | "\u3000") > //被認為是空格的字符 | <#_QUOTED_CHAR: ( ~[ "\"", "\\" ] | <_ESCAPED_CHAR> ) > //被引號括起來的字符不應(yīng)再包括"和\,當(dāng)然轉(zhuǎn)義過的除外。 } |
(2) 默認狀態(tài)的Token
<DEFAULT> TOKEN : { <AND: ("AND" | "&&") > | <OR: ("OR" | "||") > | <NOT: ("NOT" | "!") > | <PLUS: "+" > | <MINUS: "-" > | <LPAREN: "(" > | <RPAREN: ")" > | <COLON: ":" > | <STAR: "*" > | <CARAT: "^" > : Boost //當(dāng)遇到^的時候,后面跟隨的是boost表達式,進入Boost狀態(tài) | <QUOTED: "\"" (<_QUOTED_CHAR>)* "\""> | <TERM: <_TERM_START_CHAR> (<_TERM_CHAR>)* > | <FUZZY_SLOP: "~" ( (<_NUM_CHAR>)+ ( "." (<_NUM_CHAR>)+ )? )? > //Fuzzy查詢,~后面跟小數(shù)。 | <PREFIXTERM: ("*") | ( <_TERM_START_CHAR> (<_TERM_CHAR>)* "*" ) > //使用*進行Prefix查詢,可以盡包含*,或者末尾包含*,然而只包含*符合詞法,不合語法。 | <WILDTERM: (<_TERM_START_CHAR> | [ "*", "?" ]) (<_TERM_CHAR> | ( [ "*", "?" ] ))* > //使用*和?進行wildcard查詢 | <RANGEIN_START: "[" > : RangeIn //遇到[]的時候,是包含邊界的Range查詢 | <RANGEEX_START: "{" > : RangeEx //遇到{}的時候,是不包含邊界的Range查詢 } |
<Boost> TOKEN : { <NUMBER: (<_NUM_CHAR>)+ ( "." (<_NUM_CHAR>)+ )? > : DEFAULT //boost是一個小數(shù) } |
//包含邊界的Range查詢是[A TO B]的形式。 <RangeIn> TOKEN : { <RANGEIN_TO: "TO"> | <RANGEIN_END: "]"> : DEFAULT | <RANGEIN_QUOTED: "\"" (~["\""] | "\\\"")+ "\""> | <RANGEIN_GOOP: (~[ " ", "]" ])+ > } |
//不包含邊界的Range查詢是{A TO B}的形式 <RangeEx> TOKEN : { <RANGEEX_TO: "TO"> | <RANGEEX_END: "}"> : DEFAULT | <RANGEEX_QUOTED: "\"" (~["\""] | "\\\"")+ "\""> | <RANGEEX_GOOP: (~[ " ", "}" ])+ > } |
3.3、聲明語法分析器
Lucene的語法規(guī)則如下:
Query ::= ( Clause )* Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" ) |
(1) 從Query到Clause
一個Query查詢語句,是由多個clause組成的,每個clause有修飾符Modifier,或為+, 或為-,clause之間的有連接符,或為AND,或為OR,或為NOT。
在Lucene的語法解析中NOT被算作Modifier,和-起相同作用。
此過程表達式如下:
Query TopLevelQuery(String field) : { Query q; } { q=Query(field) <EOF> { return q; } } |
Query Query(String field) : { List<BooleanClause> clauses = new ArrayList<BooleanClause>(); Query q, firstQuery=null; int conj, mods; } { //查詢語句開頭是一個Modifier,可以為空 //Modifier后面便是子語句clause,可以生成子查詢語句q mods=Modifiers() q=Clause(field) { //如果第一個語句的Modifier是空,則將子查詢q付給firstQuery,從后面我們可以看到,當(dāng)只有一個查詢 語句的時候,如果其Modifier為空,則不返回BooleanQuery,而是返回子查詢對象firstQuery。從這里我們可以看出,如果查詢語 句為"A",則生成TermQuery,其term為"A",如果查詢語句為"+A",則生成BooleanQuery,其子查詢只有一個,就是 TermQuery,其term為"A"。 addClause(clauses, CONJ_NONE, mods, q); if (mods == MOD_NONE) firstQuery=q; } ( //除了第一個語句外,其他的前面可以有連接符,或為AND,或為OR。 //如果在第一個語句之前出現(xiàn)連接符,則報錯,如"OR a",會報Encountered " <OR> "OR "" at line 1, column 0. //除了連接符,也會有Modifier,后面是子語句clause,生成子查詢q,并加入BooleanQuery中。 conj=Conjunction() mods=Modifiers() q=Clause(field) { addClause(clauses, conj, mods, q); } )* { //如果只有一個查詢語句,且其modifier為空,則返回firstQuery,否則由所有的子語句clause,生成BooleanQuery。 if (clauses.size() == 1 && firstQuery != null) return firstQuery; else { return getBooleanQuery(clauses); } } } |
int Modifiers() : { //默認modifier為空,如果遇到+,就是required,如果遇到-或者NOT,就是prohibited。 int ret = MOD_NONE; } { [ <PLUS> { ret = MOD_REQ; } | <MINUS> { ret = MOD_NOT; } | <NOT> { ret = MOD_NOT; } ] { return ret; } } |
//連接符 int Conjunction() : { int ret = CONJ_NONE; } { [ <AND> { ret = CONJ_AND; } | <OR> { ret = CONJ_OR; } ] { return ret; } } |
(2) 一個子語句clause
由上面的分析我們可以知道,JavaCC使用的是編譯原理里面的自上而下分析法,基本采用的是LL(1)的方法:
- 第一個L :從左到右掃描輸入串
- 第二個L :生成的是最左推導(dǎo)
- (1):向前看一個輸入符號(lookahead)
JavaCC還提供LOOKAHEAD(n),也即當(dāng)僅讀入下一個符號時,不足以判斷接下來的如何解析,會出現(xiàn)Choice Conflict,則需要多讀入幾個符號,來進一步判斷。
Query Clause(String field) : { Query q; Token fieldToken=null, boost=null; } { //此處之所以向前看兩個符號,就是當(dāng)看到<TERM>的時候,不知道它是一個field,還是一個term,當(dāng)<TERM><COLON>在一起的時候,說明<TERM>代表一個field, 否則代表一個term [ LOOKAHEAD(2) ( fieldToken=<TERM> <COLON> {field=discardEscapeChar(fieldToken.image);} | <STAR> <COLON> {field="*";} ) ] ( //或者是一個term,則由此term生成一個查詢對象 //或者是一個由括號括起來的子查詢 //()?表示可能存在一個boost,格式為^加一個數(shù)字 q=Term(field) | <LPAREN> q=Query(field) <RPAREN> (<CARAT> boost=<NUMBER>)? ) { //如果存在boost,則設(shè)定查詢對象的boost if (boost != null) { float f = (float)1.0; try { f = Float.valueOf(boost.image).floatValue(); q.setBoost(f); } catch (Exception ignored) { } } return q; } } |
Query Term(String field) : { Token term, boost=null, fuzzySlop=null, goop1, goop2; boolean prefix = false; boolean wildcard = false; boolean fuzzy = false; Query q; } { ( ( //如果term僅結(jié)尾包含*則是prefix查詢。 //如果以*開頭,或者中間包含*,或者結(jié)尾包含*(如果僅結(jié)尾包含,則prefix優(yōu)先)則為wildcard查詢。 term=<TERM> | term=<STAR> { wildcard=true; } | term=<PREFIXTERM> { prefix=true; } | term=<WILDTERM> { wildcard=true; } | term=<NUMBER> ) //如果term后面是~,則是fuzzy查詢 [ fuzzySlop=<FUZZY_SLOP> { fuzzy=true; } ] [ <CARAT> boost=<NUMBER> [ fuzzySlop=<FUZZY_SLOP> { fuzzy=true; } ] ] { //如果是wildcard查詢,則調(diào)用getWildcardQuery, // *:*得到MatchAllDocsQuery,將返回所有的文檔 // 目前不支持最前面帶通配符的查詢(雖然詞法分析和語法分析都能通過),否則報ParseException // 最后生成WildcardQuery //如果是prefix查詢,則調(diào)用getPrefixQuery,生成PrefixQuery //如果是fuzzy查詢,則調(diào)用getFuzzyQuery,生成FuzzyQuery //如果是普通查詢,則調(diào)用getFieldQuery String termImage=discardEscapeChar(term.image); if (wildcard) { q = getWildcardQuery(field, termImage); } else if (prefix) { q = getPrefixQuery(field, discardEscapeChar(term.image.substring(0, term.image.length()-1))); } else if (fuzzy) { float fms = fuzzyMinSim; try { fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); } catch (Exception ignored) { } if(fms < 0.0f || fms > 1.0f){ throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !"); } q = getFuzzyQuery(field, termImage,fms); } else { q = getFieldQuery(field, termImage); } } //包含邊界的range查詢,取得[goop1 TO goop2],調(diào)用getRangeQuery,生成TermRangeQuery | ( <RANGEIN_START> ( goop1=<RANGEIN_GOOP>|goop1=<RANGEIN_QUOTED> ) [ <RANGEIN_TO> ] ( goop2=<RANGEIN_GOOP>|goop2=<RANGEIN_QUOTED> ) <RANGEIN_END> ) [ <CARAT> boost=<NUMBER> ] { if (goop1.kind == RANGEIN_QUOTED) { goop1.image = goop1.image.substring(1, goop1.image.length()-1); } if (goop2.kind == RANGEIN_QUOTED) { goop2.image = goop2.image.substring(1, goop2.image.length()-1); } q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), true); } //不包含邊界的range查詢,取得{goop1 TO goop2},調(diào)用getRangeQuery,生成TermRangeQuery | ( <RANGEEX_START> ( goop1=<RANGEEX_GOOP>|goop1=<RANGEEX_QUOTED> ) [ <RANGEEX_TO> ] ( goop2=<RANGEEX_GOOP>|goop2=<RANGEEX_QUOTED> ) <RANGEEX_END> ) [ <CARAT> boost=<NUMBER> ] { if (goop1.kind == RANGEEX_QUOTED) { goop1.image = goop1.image.substring(1, goop1.image.length()-1); } if (goop2.kind == RANGEEX_QUOTED) { goop2.image = goop2.image.substring(1, goop2.image.length()-1); } q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), false); } //被""括起來的term,得到phrase查詢,調(diào)用getFieldQuery | term=<QUOTED> [ fuzzySlop=<FUZZY_SLOP> ] [ <CARAT> boost=<NUMBER> ] { int s = phraseSlop; if (fuzzySlop != null) { try { s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); } catch (Exception ignored) { } } q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), s); } ) { if (boost != null) { float f = (float) 1.0; try { f = Float.valueOf(boost.image).floatValue(); } catch (Exception ignored) { } // avoid boosting null queries, such as those caused by stop words if (q != null) { q.setBoost(f); } } return q; } } |
此處需要詳細解析的是getFieldQuery:
protected Query getFieldQuery(String field, String queryText) throws ParseException {
//需要用analyzer對文本進行分詞
TokenStream source;
try {
source = analyzer.reusableTokenStream(field, new StringReader(queryText));
source.reset();
} catch (IOException e) {
source = analyzer.tokenStream(field, new StringReader(queryText));
}
CachingTokenFilter buffer = new CachingTokenFilter(source);
TermAttribute termAtt = null;
PositionIncrementAttribute posIncrAtt = null;
int numTokens = 0;
boolean success = false;
try {
buffer.reset();
success = true;
} catch (IOException e) {
}
//得到TermAttribute和PositionIncrementAttribute,此兩項將決定到底產(chǎn)生什么樣的Query對象
if (success) {
if (buffer.hasAttribute(TermAttribute.class)) {
termAtt = buffer.getAttribute(TermAttribute.class);
}
if (buffer.hasAttribute(PositionIncrementAttribute.class)) {
posIncrAtt = buffer.getAttribute(PositionIncrementAttribute.class);
}
}
int positionCount = 0;
boolean severalTokensAtSamePosition = false;
boolean hasMoreTokens = false;
if (termAtt != null) {
try {
//遍歷分詞后的所有Token,統(tǒng)計Tokens的個數(shù)numTokens,以及positionIncrement的總數(shù),即positionCount。
//當(dāng)有一次positionIncrement為0的時候,severalTokensAtSamePosition設(shè)為true,表示有多個Token處在同一個位置。
hasMoreTokens = buffer.incrementToken();
while (hasMoreTokens) {
numTokens++;
int positionIncrement = (posIncrAtt != null) ? posIncrAtt.getPositionIncrement() : 1;
if (positionIncrement != 0) {
positionCount += positionIncrement;
} else {
severalTokensAtSamePosition = true;
}
hasMoreTokens = buffer.incrementToken();
}
} catch (IOException e) {
}
}
try {
//重設(shè)buffer,以便生成phrase查詢的時候,term和position可以重新遍歷。
buffer.reset();
source.close();
}
catch (IOException e) {
}
if (numTokens == 0)
return null;
else if (numTokens == 1) {
//如果分詞后只有一個Token,則生成TermQuery
String term = null;
try {
boolean hasNext = buffer.incrementToken();
term = termAtt.term();
} catch (IOException e) {
}
return newTermQuery(new Term(field, term));
} else {
//如果分詞后不只有一個Token
if (severalTokensAtSamePosition) {
//如果有多個Token處于同一個位置
if (positionCount == 1) {
//并且處于同一位置的Token還全部處于第一個位置,則生成BooleanQuery,處于同一位置的Token之間是OR的關(guān)系
BooleanQuery q = newBooleanQuery(true);
for (int i = 0; i < numTokens; i++) {
String term = null;
try {
boolean hasNext = buffer.incrementToken();
term = termAtt.term();
} catch (IOException e) {
}
Query currentQuery = newTermQuery(new Term(field, term));
q.add(currentQuery, BooleanClause.Occur.SHOULD);
}
return q;
}
else {
//如果有多個Token處于同一位置,但不是第一個位置,則生成MultiPhraseQuery。
//所謂MultiPhraseQuery即其可以包含多個phrase,其又一個ArrayList<Term[]> termArrays,每一項都是一個Term的數(shù)組,屬于同一個數(shù)組的Term表示在同一個位置。它有函數(shù)void add(Term[] terms)一次添加一個數(shù)組的Term。比如我們要搜索"microsoft app*",其表示多個phrase,"microsoft apple","microsoft application"都算。此時用QueryParser.parse("\"microsoft app*\"")從而生成PhraseQuery是搜不出microsoft apple和microsoft application的,也不能搜出microsoft app,因為*一旦被引號所引,就不算通配符了。所以必須生成MultiPhraseQuery,首先用add(new Term[]{new Term("field", "microsoft")})將microsoft作為一個Term數(shù)組添加進去,然后用add(new Term[]{new Term("field", "app"), new Term("field", "apple"), new Term("field", "application")})作為一個Term數(shù)組添加進去(算作同一個位置的),則三者都能搜的出來。
MultiPhraseQuery mpq = newMultiPhraseQuery();
mpq.setSlop(phraseSlop);
List<Term> multiTerms = new ArrayList<Term>();
int position = -1;
for (int i = 0; i < numTokens; i++) {
String term = null;
int positionIncrement = 1;
try {
boolean hasNext = buffer.incrementToken();
assert hasNext == true;
term = termAtt.term();
if (posIncrAtt != null) {
positionIncrement = posIncrAtt.getPositionIncrement();
}
} catch (IOException e) {
}
if (positionIncrement > 0 && multiTerms.size() > 0) {
//如果positionIncrement大于零,說明此Term和前一個Term已經(jīng)不是同一個位置 了,所以原來收集在multiTerms中的Term都算作同一個位置,添加到MultiPhraseQuery中作為一項。并清除 multiTerms,以便重新收集相同位置的Term。
if (enablePositionIncrements) {
mpq.add(multiTerms.toArray(new Term[0]),position);
} else {
mpq.add(multiTerms.toArray(new Term[0]));
}
multiTerms.clear();
}
//將此Term收集到multiTerms中。
position += positionIncrement;
multiTerms.add(new Term(field, term));
}
//當(dāng)遍歷完所有的Token,同處于最后一個位置的Term已經(jīng)收集到multiTerms中了,把他們加到MultiPhraseQuery中作為一項。
if (enablePositionIncrements) {
mpq.add(multiTerms.toArray(new Term[0]),position);
} else {
mpq.add(multiTerms.toArray(new Term[0]));
}
return mpq;
}
}
else {
//如果不存在多個Token處于同一個位置的情況,則直接生成PhraseQuery
PhraseQuery pq = newPhraseQuery();
pq.setSlop(phraseSlop);
int position = -1;
for (int i = 0; i < numTokens; i++) {
String term = null;
int positionIncrement = 1;
try {
boolean hasNext = buffer.incrementToken();
assert hasNext == true;
term = termAtt.term();
if (posIncrAtt != null) {
positionIncrement = posIncrAtt.getPositionIncrement();
}
} catch (IOException e) {
}
if (enablePositionIncrements) {
position += positionIncrement;
pq.add(new Term(field, term),position);
} else {
pq.add(new Term(field, term));
}
}
return pq;
}
}
}