Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser
一、Lucene的查詢語法
Lucene所支持的查詢語法可見http://lucene.apache.org/java/3_0_1/queryparsersyntax.html
(1) 語法關鍵字
+ - && || ! ( ) { } [ ] ^ " ~ * ? : /
如果所要查詢的查詢詞中本身包含關鍵字,則需要用/進行轉義
(2) 查詢詞(Term)
Lucene支持兩種查詢詞,一種是單一查詢詞,如"hello",一種是詞組(phrase),如"hello world"。
(3) 查詢域(Field)
在查詢語句中,可以指定從哪個域中尋找查詢詞,如果不指定,則從默認域中查找。
查詢域和查詢詞之間用:分隔,如title:"Do it right"。
:僅對緊跟其后的查詢詞起作用,如果title:Do it right,則僅表示在title中查詢Do,而it right要在默認域中查詢。
(4) 通配符查詢(Wildcard)
支持兩種通配符:?表示一個字符,*表示多個字符。
通配符可以出現(xiàn)在查詢詞的中間或者末尾,如te?t,test*,te*t,但決不能出現(xiàn)在開始,如*test,?test。
(5) 模糊查詢(Fuzzy)
模糊查詢的算法是基于Levenshtein Distance,也即當兩個詞的差別小于某個比例的時候,就算匹配,如roam~0.8,即表示差別小于0.2,相似度大于0.8才算匹配。
(6) 臨近查詢(Proximity)
在詞組后面跟隨~10,表示詞組中的多個詞之間的距離之和不超過10,則滿足查詢。
所謂詞之間的距離,即查詢詞組中詞為滿足和目標詞組相同的最小移動次數(shù)。
如索引中有詞組"apple boy cat"。
如果查詢詞為"apple boy cat"~0,則匹配。
如果查詢詞為"boy apple cat"~2,距離設為2方能匹配,設為1則不能匹配。
| (0) | boy | apple | cat |
| (1) | ? | boy apple | cat |
| (2) | apple | boy | cat |
如果查詢詞為"cat boy apple"~4,距離設為4方能匹配。
| (0) | cat | boy | apple |
| (1) | ? | cat boy | apple |
| (2) | ? | boy | cat apple |
| (3) | ? | boy apple | cat |
| (4) | apple | boy | cat |
?
(7) 區(qū)間查詢(Range)
區(qū)間查詢包含兩種,一種是包含邊界,用[A TO B]指定,一種是不包含邊界,用{A TO B}指定。
如date:[20020101 TO 20030101],當然區(qū)間查詢不僅僅用于時間,如title:{Aida TO Carmen}
(8) 增加一個查詢詞的權重(Boost)
可以在查詢詞后面加^N來設定此查詢詞的權重,默認是1,如果N大于1,則說明此查詢詞更重要,如果N小于1,則說明此查詢詞更不重要。
如jakarta^4 apache,"jakarta apache"^4 "Apache Lucene"
(9) 布爾操作符
布爾操作符包括連接符,如AND,OR,和修飾符,如NOT,+,-。
默認狀態(tài)下,空格被認為是OR的關系,QueryParser.setDefaultOperator(Operator.AND)設置為空格為AND。
+表示一個查詢語句是必須滿足的(required),NOT和-表示一個查詢語句是不能滿足的(prohibited)。
(10) 組合
可以用括號,將查詢語句進行組合,從而設定優(yōu)先級。
如(jakarta OR apache) AND website
?
Lucene的查詢語法是由QueryParser來進行解析,從而生成查詢對象的。
通過編譯原理我們知道,解析一個語法表達式,需要經過詞法分析和語法分析的過程,也即需要詞法分析器和語法分析器。
QueryParser是通過JavaCC來生成詞法分析器和語法分析器的。
?
二、JavaCC介紹
本節(jié)例子基本出于JavaCC tutorial的文章,http://www.engr.mun.ca/~theo/JavaCC-Tutorial/
JavaCC是一個詞法分析器和語法分析器的生成器。
所謂詞法分析器就是將一系列字符分成一個個的Token,并標記Token的分類。
例如,對于下面的C語言程序:
| int main() { ??? return 0 ; } |
????
將被分成以下的Token:
| “int”, “ ”, “main”, “(”, “)”, “”,“{”, “/n”, “/t”, “return” “”,“0”,“”,“;”,“/n”, “}”, “/n”, “” |
標記了Token的類型后如下:
| KWINT, SPACE, ID, OPAR, CPAR, SPACE, OBRACE, SPACE, SPACE, KWRETURN, SPACE, OCTALCONST, SPACE, SEMICOLON, SPACE, CBRACE, SPACE, EOF |
EOF表示文件的結束。
詞法分析器工作過程如圖所示:
?
?
此一系列Token將被傳給語法分析器(當然并不是所有的Token都會傳給語法分析器,本例中SPACE就例外),從而形成一棵語法分析樹來表示程序的結構。
?
JavaCC本身既不是一個詞法分析器,也不是一個語法分析器,而是根據(jù)指定的規(guī)則生成兩者的生成器。
2.1、第一個實例——正整數(shù)相加
下面我們來看第一個例子,即能夠解析正整數(shù)相加的表達式,例如99+42+0+15。
(1) 生成一個adder.jj文件
此文件中寫入的即生成詞法分析器和語法分析器的規(guī)則。
(2) 設定選項,并聲明類
| ? /* adder.jj Adding up numbers */ options { ? STATIC = false ; } PARSER_BEGIN(Adder) class Adder { ? static void main( String[] args ) throws ParseException, TokenMgrError { ??? Adder parser = new Adder( System.in ) ; ??? parser.Start() ; ? } } PARSER_END(Adder) |
STATIC選項默認是true,設為false,使得生成的函數(shù)不是static的。
PARSER_BEGIN和PARSER_END之間的java代碼部分,此部分不需要通過JavaCC根據(jù)規(guī)則生成java代碼,而是直接拷貝到生成的java代碼中的。
(3) 聲明一個詞法分析器
| SKIP : { " " } SKIP : { "/n" | "/r" | "/r/n" } TOKEN : { < PLUS : "+" > } TOKEN : { < NUMBER : (["0"-"9"])+ > } |
第一二行表示空格和回車換行是不會傳給語法分析器的。
第三行聲明了一個Token,名稱為PLUS,符號為“+”。
第四行聲明了一個Token,名稱為NUMBER,符號位一個或多個0-9的數(shù)的組合。
如果詞法分析器分析的表達式如下:
- “123 + 456/n”,則分析為NUMBER, PLUS, NUMBER, EOF
- “123 - 456/n”,則報TokenMgrError,因為“-”不是一個有效的Token.
- “123 ++ 456/n”,則分析為NUMBER, PLUS, PLUS, NUMBER, EOF,詞法分析正確,后面的語法分析將會錯誤。
(4) 聲明一個語法分析器
| void Start() : {} { ? <NUMBER> ? ( ??? <PLUS> ??? <NUMBER> ? )* ? <EOF> } |
語法分析器使用BNF表達式。
上述聲明將生成start函數(shù),稱為Adder類的一個成員函數(shù)
語法分析器要求輸入的語句必須以NUMBER開始,以EOF結尾,中間是零到多個PLUS和NUMBER的組合。
(5) 用javacc編譯adder.jj來生成語法分析器和詞法分析器
最后生成的adder.jj如下:
| options? PARSER_BEGIN(Adder)? public class Adder? SKIP :? TOKEN : /* OPERATORS */? TOKEN :? void start() :? |
用JavaCC編譯adder.jj生成如下文件:
- Adder.java:語法分析器。其中的main函數(shù)是完全從adder.jj中拷貝的,而start函數(shù)是被javacc由adder.jj描述的規(guī)則生成的。
- AdderConstants.java:一些常量,如PLUS, NUMBER, EOF等。
- AdderTokenManager.java:詞法分析器。
- ParseException.java:用于在語法分析錯誤的時候拋出。
- SimpleCharStream.java:用于將一系列字符串傳入詞法分析器。
- Token.java:代表詞法分析后的一個個Token。Token對象有一個整型域kind,來表示此Token的類型(PLUS, NUMBER, EOF),有一個String類型的域image,來表示此Token的值。
- TokenMgrError.java:用于在詞法分析錯誤的時候拋出。
下面我們對adder.jj生成的start函數(shù)進行分析:
| final public void start() throws ParseException { ??//從詞法分析器取得下一個Token,而且要求必須是NUMBER類型,否則拋出異常。 ??//此步要求表達式第一個出現(xiàn)的字符必須是NUMBER。 ? jj_consume_token(NUMBER); ? label_1: ? while (true) { ????//jj_ntk()是取得下一個Token的類型,如果是PLUS,則繼續(xù)進行,如果是EOF則退出循環(huán)。 ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case PLUS: ????? ; ????? break; ??? default: ????? jj_la1[0] = jj_gen; ????? break label_1; ??? } ???//要求下一個PLUS字符,再下一個是一個NUMBER,如此下去。 ??? jj_consume_token(PLUS); ??? jj_consume_token(NUMBER); ? } } |
(6) 運行Adder.java
如果輸入“123+456”則不報任何錯誤。
如果輸入“123++456”則報如下異常:
| Exception in thread "main" org.apache.javacc.ParseException: Encountered " "+" "+ "" at line 1, column 5.? |
如果輸入“123-456”則報如下異常:
| Exception in thread "main" org.apache.javacc.TokenMgrError: Lexical error at line 1, column 4.? Encountered: "-" (45), after : ""? |
2.2、擴展語法分析器
在上面的例子中的start函數(shù)中,我們僅僅通過語法分析器來判斷輸入的語句是否正確。
我們可以擴展BNF表達式,加入Java代碼,使得經過語法分析后,得到我們想要的結果或者對象。
我們將start函數(shù)改寫為:
| int start() throws NumberFormatException : { ??//start函數(shù)中有三個變量 ? Token t ; ? int i ; ? int value ; } { ??//首先要求表達式的第一個一定是一個NUMBER,并把其值付給t ? t= <NUMBER> ??//將t的值取出來,解析為整型,放入變量i中 ? { i = Integer.parseInt( t.image ) ; } ??//最后的結果value設為i ? { value = i ; } ??//緊接著應該是零個或者多個PLUS和NUMBER的組合 ? ( ??? <PLUS> ????//每出現(xiàn)一個NUMBER,都將其付給t,并將t的值解析為整型,付給i ??? t= <NUMBER> ??? { i = Integer.parseInt( t.image ) ; } ????//將i加到value上 ??? { value += i ; } ? )* ??//最后的value就是表達式的和 ? { return value ; } } |
生成的start函數(shù)如下:
| final public int start() throws ParseException, NumberFormatException { ? Token t; ? int i; ? int value; ? t = jj_consume_token(NUMBER); ? i = Integer.parseInt(t.image); ? value = i; ? label_1: while (true) { ??? switch ((jj_ntk == -1) ? jj_ntk() : jj_ntk) { ??? case PLUS: ????? ; ????? break; ??? default: ????? jj_la1[0] = jj_gen; ????? break label_1; ??? } ??? jj_consume_token(PLUS); ??? t = jj_consume_token(NUMBER); ??? i = Integer.parseInt(t.image); ??? value += i; ? } ? { ??? if (true) ????? return value; ? } ? throw new Error("Missing return statement in function"); } |
從上面的例子,我們發(fā)現(xiàn),把一個NUMBER取出,并解析為整型這一步是可以共用的,所以可以抽象為一個函數(shù):
| int start() throws NumberFormatException : { ? int i; ? int value ; } { ? value = getNextNumberValue() ? ( ??? <PLUS> ??? i = getNextNumberValue() ??? { value += i ; } ? )* ? { return value ; } } int getNextNumberValue() throws NumberFormatException : { ? Token t ; } { ? t=<NUMBER> ? { return Integer.parseInt( t.image ) ; } } |
生成的函數(shù)如下:
| ? final public int start() throws ParseException, NumberFormatException { ? int i; ? int value; ? value = getNextNumberValue(); ? label_1: while (true) { ??? switch ((jj_ntk == -1) ? jj_ntk() : jj_ntk) { ??? case PLUS: ????? ; ????? break; ??? default: ????? jj_la1[0] = jj_gen; ????? break label_1; ??? } ??? jj_consume_token(PLUS); ??? i = getNextNumberValue(); ??? value += i; ? } ? { ??? if (true) ????? return value; ? } ? throw new Error("Missing return statement in function"); } final public int getNextNumberValue() throws ParseException, NumberFormatException { ? Token t; ? t = jj_consume_token(NUMBER); ? { ??? if (true) ????? return Integer.parseInt(t.image); ? } ? throw new Error("Missing return statement in function"); } |
?
2.3、第二個實例:計算器
(1) 生成一個calculator.jj文件
用于寫入生成計算器詞法分析器和語法分析器的規(guī)則。
(2) 設定選項,并聲明類
| options { STATIC = false ; } PARSER_BEGIN(Calculator) ? import java.io.PrintStream ; ? class Calculator { ??? static void main( String[] args ) throws ParseException, TokenMgrError, NumberFormatException { ????? Calculator parser = new Calculator( System.in ) ; ????? parser.Start( System.out ) ; ??? } ??? double previousValue = 0.0 ; ? } PARSER_END(Calculator) |
previousValue用來記錄上一次計算的結果。
(3) 聲明一個詞法分析器
| SKIP : { " " } TOKEN : { < EOL:"/n" | "/r" | "/r/n" > } TOKEN : { < PLUS : "+" > } |
我們想要支持小數(shù),則有四種情況:沒有小數(shù),小數(shù)點在中間,小數(shù)點在前面,小數(shù)點在后面。則語法規(guī)則如下:
| TOKEN { < NUMBER : (["0"-"9"])+ | (["0"-"9"])+ "." (["0"-"9"])+ | (["0"-"9"])+ "." | "." (["0"-"9"])+ > } |
由于同一個表達式["0"-"9"]使用了多次,因而我們可以定義變量,如下:
| TOKEN : { < NUMBER : <DIGITS> | <DIGITS> "." <DIGITS> | <DIGITS> "." | "." <DIGITS>> } TOKEN : { < #DIGITS : (["0"-"9"])+ > } |
(4) 聲明一個語法分析器
我們想做的計算器包含多行,每行都是一個四則運算表達式,語法規(guī)則如下:
| Start -> (Expression EOL)* EOF |
| void Start(PrintStream printStream) throws NumberFormatException : {} { ? ( ??? previousValue = Expression() ??? <EOL> ??? { printStream.println( previousValue ) ; } ? )* ? <EOF> } |
每一行的四則運算表達式如果只包含加法,則語法規(guī)則如下:
| Expression -> Primary (PLUS Primary)* |
| double Expression() throws NumberFormatException : { ? double i ; ? double value ; } { ? value = Primary() ? ( ??? <PLUS> ??? i= Primary() ??? { value += i ; } ? )* ? { return value ; } } |
其中Primary()得到一個數(shù)的值:
| double Primary() throws NumberFormatException : { ? Token t ; } { ? t= <NUMBER> ? { return Double.parseDouble( t.image ) ; } } |
(5) 擴展詞法分析器和語法分析器
如果我們想支持減法,則需要在詞法分析器中添加:
| TOKEN : { < MINUS : "-" > } |
語法分析器應該變?yōu)?#xff1a;
| Expression -> Primary (PLUS Primary | MINUS Primary)* |
| double Expression() throws NumberFormatException : { ? double i ; ? double value ; } { ? value = Primary() ? ( ??? <PLUS> ??? i = Primary() ??? { value += i ; } ??? | ??? <MINUS> ??? i = Primary() ??? { value -= i ; } ? )* ? { return value ; } } |
如果我們想添加乘法和除法,則在詞法分析器中應該加入:
| TOKEN : { < TIMES : "*" > } TOKEN : { < DIVIDE : "/" > } |
對于加減乘除混合運算,則應該考慮優(yōu)先級,乘除的優(yōu)先級高于加減,應該先做乘除,再做加減:
| Expression -> Term (PLUSTerm | MINUSTerm)* Term -> Primary (TIMES Primary | DIVIDE Primary)* |
| double Expression() throws NumberFormatException : { ? double i ; ? double value ; } { ? value = Term() ? ( ??? <PLUS> ??? i= Term() ??? { value += i ; } ??? | ??? <MINUS> ??? i= Term() ??? { value -= i ; } ? )* ? { return value ; } } |
| double Term() throws NumberFormatException : { ? double i ; ? double value ; } { ? value = Primary() ? ( ??? <TIMES> ??? i = Primary() ??? { value *= i ; } ??? | ??? <DIVIDE> ??? i = Primary() ??? { value /= i ; } ? )* ? { return value ; } } |
下面我們要開始支持括號,負號,以及取得上一行四則運算表達式的值。
對于詞法分析器,我們添加如下Token:
| TOKEN : { < OPEN PAR : "(" > } TOKEN : { < CLOSE PAR : ")" > } TOKEN : { < PREVIOUS : "$" > } |
對于語法分析器,對于最基本的表達式,有四種情況:
其可以是一個NUMBER,也可以是上一行四則運算表達式的值PREVIOUS,也可以是被括號括起來的一個子語法表達式,也可以是取負的一個基本語法表達式。
| Primary –> NUMBER | PREVIOUS | OPEN_PAR Expression CLOSE_PAR | MINUS Primary |
| double Primary() throws NumberFormatException : { ? Token t ; ? double d ; } { ? t=<NUMBER> ? { return Double.parseDouble( t.image ) ; } ? | ? <PREVIOUS> ? { return previousValue ; } ? | ? <OPEN PAR> d=Expression() <CLOSE PAR> ? { return d ; } ? | ? <MINUS> d=Primary() ? { return -d ; } } |
(6) 用javacc編譯calculator.jj來生成語法分析器和詞法分析器
最后生成的calculator.jj如下:
| options? PARSER_BEGIN(Calculator)? SKIP : { " " }? void start(PrintStream printStream) throws NumberFormatException :? double Expression() throws NumberFormatException :? double Term() throws NumberFormatException :? double Primary() throws NumberFormatException :? |
生成的start函數(shù)如下:
| final public void start(PrintStream printStream) throws ParseException, NumberFormatException { ? label_1: ? while (true) { ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case MINUS: ??? case NUMBER: ??? case OPEN_PAR: ??? case PREVIOUS: ????? ; ????? break; ??? default: ????? jj_la1[0] = jj_gen; ????? break label_1; ??? } ??? previousValue = Expression(); ??? printStream.println( previousValue ) ; ? } } final public double Expression() throws ParseException, NumberFormatException { ? double i ; ? double value ; ? value = Term(); ? label_2: ? while (true) { ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case PLUS: ??? case MINUS: ????? ; ????? break; ??? default: ????? jj_la1[1] = jj_gen; ????? break label_2; ??? } ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case PLUS: ????? jj_consume_token(PLUS); ????? i = Term(); ????? value += i ; ????? break; ??? case MINUS: ????? jj_consume_token(MINUS); ????? i = Term(); ????? value -= i ; ????? break; ??? default: ????? jj_la1[2] = jj_gen; ????? jj_consume_token(-1); ????? throw new ParseException(); ??? } ? } ? {if (true) return value ;} ? throw new Error("Missing return statement in function"); } final public double Term() throws ParseException, NumberFormatException { ? double i ; ? double value ; ? value = Primary(); ? label_3: ? while (true) { ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case TIMES: ??? case DIVIDE: ????? ; ????? break; ??? default: ????? jj_la1[3] = jj_gen; ????? break label_3; ??? } ??? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ??? case TIMES: ????? jj_consume_token(TIMES); ????? i = Primary(); ????? value *= i ; ????? break; ??? case DIVIDE: ????? jj_consume_token(DIVIDE); ????? i = Primary(); ????? value /= i ; ????? break; ??? default: ????? jj_la1[4] = jj_gen; ????? jj_consume_token(-1); ????? throw new ParseException(); ??? } ? } ? {if (true) return value ;} ? throw new Error("Missing return statement in function"); } final public double Primary() throws ParseException, NumberFormatException { ? Token t ; ? double d ; ? switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { ? case NUMBER: ??? t = jj_consume_token(NUMBER); ??? {if (true) return Double.parseDouble( t.image ) ;} ??? break; ? case PREVIOUS: ??? jj_consume_token(PREVIOUS); ??? {if (true) return previousValue ;} ??? break; ? case OPEN_PAR: ??? jj_consume_token(OPEN_PAR); ??? d = Expression(); ??? jj_consume_token(CLOSE_PAR); ??? {if (true) return d ;} ??? break; ? case MINUS: ??? jj_consume_token(MINUS); ??? d = Primary(); ??? {if (true) return -d ;} ??? break; ? default: ??? jj_la1[5] = jj_gen; ??? jj_consume_token(-1); ??? throw new ParseException(); ? } ? throw new Error("Missing return statement in function"); } |
三、解析QueryParser.jj
?
3.1、聲明QueryParser類
在QueryParser.jj文件中,PARSER_BEGIN(QueryParser)和PARSER_END(QueryParser)之間,定義了QueryParser類。
其中最重要的一個函數(shù)是public Query parse(String query)函數(shù),也即我們解析Lucene查詢語法的時候調用的函數(shù)。
這是一個純Java代碼定義的函數(shù),會直接拷貝到QueryParser.java文件中。
parse函數(shù)中,最重要的一行代碼是調用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ī)則,然而當出現(xiàn)"/**"的時候,其后面的表達式則不需要滿足java語言的語法規(guī)則,而是應該滿足java注釋的語法規(guī)則(要識別@param變量等),于是我們做如下定義:
| //默認處于DEFAULT狀態(tài),當遇到/**的時候,轉換為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 } |
<*> 表示應用于任何狀態(tài)。
(1) 應用于所有狀態(tài)的變量
| <*> TOKEN : { ? <#_NUM_CHAR:?? ["0"-"9"] >?//數(shù)字 | <#_ESCAPED_CHAR: "//" ~[] >?//"/"后的任何一個字符都是被轉義的 | <#_TERM_START_CHAR: ( ~[ " ", "/t", "/n", "/r", "/u3000", "+", "-", "!", "(", ")", ":", "^", "[", "]", "/"", "{", "}", "~", "*", "?", "//" ] | <_ESCAPED_CHAR> ) >?//表達式中任何一個term,都不能以[]括起來的列表中的lucene查詢語法關鍵字開頭,當然被轉義的除外。 | <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) >?//表達式中的term非起始字符,可以包含任何非語法關鍵字字符,轉義過的字符,也可以包含+, -(但包含+,-的符合詞法,不合語法)。 | <#_WHITESPACE: ( " " | "/t" | "/n" | "/r" | "/u3000") >?//被認為是空格的字符 | <#_QUOTED_CHAR: ( ~[ "/"", "//" ] | <_ESCAPED_CHAR> ) >?//被引號括起來的字符不應再包括"和/,當然轉義過的除外。 } |
?
(2) 默認狀態(tài)的Token
| <DEFAULT> TOKEN : { ? <AND:?????? ("AND" | "&&") > | <OR:??????? ("OR" | "||") > | <NOT:?????? ("NOT" | "!") > | <PLUS:????? "+" > | <MINUS:???? "-" > | <LPAREN:??? "(" > | <RPAREN:??? ")" > | <COLON:???? ":" > | <STAR:????? "*" > | <CARAT:???? "^" > : Boost?//當遇到^的時候,后面跟隨的是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,從后面我們可以看到,當只有一個查詢語句的時候,如果其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 :生成的是最左推導
- (1):向前看一個輸入符號(lookahead)
JavaCC還提供LOOKAHEAD(n),也即當僅讀入下一個符號時,不足以判斷接下來的如何解析,會出現(xiàn)Choice Conflict,則需要多讀入幾個符號,來進一步判斷。
| ? Query Clause(String field) : { ? Query q; ? Token fieldToken=null, boost=null; } { ??//此處之所以向前看兩個符號,就是當看到<TERM>的時候,不知道它是一個field,還是一個term,當<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,則設定查詢對象的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僅結尾包含*則是prefix查詢。 ?????? //如果以*開頭,或者中間包含*,或者結尾包含*(如果僅結尾包含,則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查詢,則調用getWildcardQuery, ??????? //??? *:*得到MatchAllDocsQuery,將返回所有的文檔 ??????? //??? 目前不支持最前面帶通配符的查詢(雖然詞法分析和語法分析都能通過),否則報ParseException ??????? //??? 最后生成WildcardQuery ??????? //如果是prefix查詢,則調用getPrefixQuery,生成PrefixQuery ??????? //如果是fuzzy查詢,則調用getFuzzyQuery,生成FuzzyQuery ??????? //如果是普通查詢,則調用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],調用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},調用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查詢,調用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,此兩項將決定到底產生什么樣的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。 ????? //當有一次positionIncrement為0的時候,severalTokensAtSamePosition設為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 { ????//重設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的關系 ??????? 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已經不是同一個位置了,所以原來收集在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)); ??????? } ????????//當遍歷完所有的Token,同處于最后一個位置的Term已經收集到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; ??? } ? } } |
總結
以上是生活随笔為你收集整理的Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Lucene学习总结之七:Lucene搜
- 下一篇: apche commons项目简介