1. 程式人生 > >luncene 查詢字串的解析—QueryParser類

luncene 查詢字串的解析—QueryParser類




搜尋流程中的第二步就是構建一個Query。下面就來介紹Query及其構建。

當用戶輸入一個關鍵字,搜尋引擎接收到後,並不是立刻就將它放入後臺開始進行關鍵字的檢索,而應當首先對這個關鍵字進行一定的分析和處理,使之成為一種後臺可以理解的形式,只有這樣,才能提高檢索的效率,同時檢索出更加有效的結果。那麼,在Lucene中,這種處理,其實就是構建一個Query物件。

Query物件本身言,它只是Lucenesearch包中的一個抽象類,這個抽象類有許多子類,代表了不同型別的檢索。如常見的TermQuery就是將一個簡單的關鍵字進行封裝後的物件,類似的還有BooleanQuery,即布林型的查詢。

IndexSearcher物件的search方法中總是需要一個Query物件(或是Query子類的物件),本節就來介紹各種Query類。

TermQuery是最簡單、也是最常用的QueryTermQuery可以理解成為“詞條搜尋”,在搜尋引擎中最基本的搜尋就是在索引中搜索某一詞條,而TermQuery就是用來完成這項工作的。

Lucene中詞條是最基本的搜尋單位,從本質上來講一個詞條其實就是一個名/值對。只不過這個“名”是欄位名,而“值”則表示欄位中所包含的某個關鍵字。

要使用TermQuery進行搜尋首先需要構造一個Term物件,示例程式碼如下:

Term aTerm = new Term("contents", "java")

然後使用aTerm物件為引數來構造一個TermQuery物件,程式碼設定如下:

Query query = new TermQuery(aTerm)

這樣所有在“contents”欄位中包含有“java”的文件都會在使用TermQuery進行查詢時作為符合查詢條件的結果返回。

下面就通過程式碼11.4來介紹TermQuery的具體實現過程。

程式碼11.4  TermQueryTest.java

package ch11;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.Term;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.TermQuery;

public class TermQueryTest

{

  public static void main(String[] args) throws Exception

  {

     //生成Document物件

    Document doc1 = new Document();

     //新增“name”欄位的內容

    doc1.add(Field.Text("name", "word1 word2 word3"));

     //新增“title”欄位的內容

    doc1.add(Field.Keyword("title", "doc1"));

     //生成索引書寫器

    IndexWriter writer = new IndexWriter("c://index", new StandardAnalyzer(), true);

    //將文件新增到索引中

    writer.addDocument(doc1);

     //關閉索引

    writer.close();

     //生成查詢物件query

    Query query = null;

    //生成hits結果物件,儲存返回的檢索結果

    Hits hits = null;

     //生成檢索器

    IndexSearcher searcher = new IndexSearcher("c://index");

     // 構造一個TermQuery物件

    query = new TermQuery(new Term("name","word1"));

     //開始檢索,並返回檢索結果到hits

    hits = searcher.search(query);

     //輸出檢索結果中的相關資訊

    printResult(hits, "word1");

     // 再次構造一個TermQuery物件,只不過查詢的欄位變成了"title"

    query = new TermQuery(new Term("title","doc1"));

     //開始第二次檢索,並返回檢索結果到hits

    hits = searcher.search(query);

     //輸出檢索結果中的相關資訊

    printResult(hits, "doc1");

  }

  public static void printResult(Hits hits, String key) throws Exception

  {

    System.out.println("查詢 /"" + key + "/" :");

    if (hits != null)

    {

      if (hits.length() == 0)

      {

        System.out.println("沒有找到任何結果");

      }

      else

      {

        System.out.println("找到" + hits.length() + "個結果");

        for (int i = 0; i < hits.length(); i++)

        {

          Document d = hits.doc(i);

          String dname = d.get("title");

          System.out.print(dname + "   ");

        }

        System.out.println();

        System.out.println();

      }

    }

  }

}

在程式碼11.4中使用TermQuery進行檢索的執行結果如圖11-8所示。

文字框:
圖11-8  TermQuery的測試注意:欄位值是區分大小寫的,因此在查詢時必須注意大小寫的匹配。

從圖11-8中可以看出,程式碼11.4兩次分別以“word1”和“doc1”為關鍵字進行檢索,並且都只得到了一個檢索結果。

在程式碼11.4中通過構建TermQuery的物件,兩次完成了對關鍵字的查詢。兩次查詢過程中不同的是,第一次構建的TermQuery是查詢“name”這個欄位,而第二次構建的TermQuery則查詢的是“title”這個欄位。

BooleanQuery也是實際開發過程中經常使用的一種Query。它其實是一個組合的Query,在使用時可以把各種Query物件新增進去並標明它們之間的邏輯關係。在本節中所討論的所有查詢型別都可以使用BooleanQuery綜合起來。BooleanQuery本身來講是一個布林子句的容器,它提供了專門的API方法往其中新增子句,並標明它們之間的關係,以下程式碼為BooleanQuery提供的用於新增子句的API介面:

public void add(Query query, boolean required, boolean prohibited)

注意BooleanQuery是可以巢狀的,一個BooleanQuery可以成為另一個BooleanQuery的條件子句。

下面以11.5為例來介紹進行“與”操作的布林型查詢。

程式碼11.5  BooleanQueryTest1.java

package ch11;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.Term;

import org.apache.lucene.search.BooleanQuery;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.TermQuery;

public class BooleanQueryTest1

{

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

     //生成新的Document物件

    Document doc1 = new Document();

    doc1.add(Field.Text("name", "word1 word2 word3"));

    doc1.add(Field.Keyword("title", "doc1"));

    Document doc2 = new Document();

    doc2.add(Field.Text("name", "word1 word4 word5"));

    doc2.add(Field.Keyword("title", "doc2"));

    Document doc3 = new Document();

    doc3.add(Field.Text("name", "word1 word2 word6"));

    doc3.add(Field.Keyword("title", "doc3"));

     //生成索引書寫器

    IndexWriter writer = new IndexWriter("c://index", new StandardAnalyzer(), true);

     //新增到索引中

    writer.addDocument(doc1);

    writer.addDocument(doc2);

    writer.addDocument(doc3);

    writer.close();

    Query query1 = null;

    Query query2 = null;

    BooleanQuery query = null;

    Hits hits = null;

     //生成IndexSearcher物件

    IndexSearcher searcher = new IndexSearcher("c://index");

    query1 = new TermQuery(new Term("name","word1"));

    query2 = new TermQuery(new Term("name","word2"));

    // 構造一個布林查詢

    query = new BooleanQuery();

    // 新增兩個子查詢

    query.add(query1, true, false);

    query.add(query2, true, false);

    hits = searcher.search(query);

    printResult(hits, "word1word2");

  }

  public static void printResult(Hits hits, String key) throws Exception

  {

    System.out.println("查詢 /"" + key + "/" :");

    if (hits != null)

    {

      if (hits.length() == 0)

      {

        System.out.println("沒有找到任何結果");

      }

      else

      {

        System.out.println("找到" + hits.length() + "個結果");

        for (int i = 0; i < hits.length(); i++)

        {

          Document d = hits.doc(i);

          String dname = d.get("title");

          System.out.print(dname + "   ");

        }

        System.out.println();

        System.out.println();

      }

    }

  }

}

程式碼11.5首先構造了兩個TermQuery,然後構造了一個BooleanQuery的物件,並將兩個TermQuery當成它的查詢子句加入Boolean查詢中。

再來看一下BooleanQueryadd方法,除了它的第一個引數外,它還有另外兩個布林型的引數。第1個引數的意思是當前所加入的查詢子句是否必須滿足,第2個引數的意思是當前所加入的查詢子句是否不需要滿足。這樣,當這兩個引數分別選擇truefalse時,會有4種不同的組合。

     true false:表明當前加入的子句是必須要滿足的。

     falsetrue:表明當前加入的子句是不可以被滿足的。

     falsefalse:表明當前加入的子句是可選的。

     truetrue:錯誤的情況。

由前面的示例可以看出由於加入的兩個子句都選用了truefalse的組合,因此它們兩個都是需要被滿足的,也就構成了實際上的“與”關係,執行效果如圖11-9所示。

如果是要進行“或”運算,則可按如下程式碼來構建查詢子句:

query.add(query1, false, false);

query.add(query2, false, false);

程式碼的執行效果如圖11-10所示。

                

11-9  BooleanQuery測試1                          11-10  BooleanQuery測試2

由於布林型的查詢是可以巢狀的,因此可以表示多種條件下的組合。不過,如果子句的數目太多,可能會導致查詢效率的降低。因此,Lucene給出了一個預設的限制,就是布林型Query的子句數目不能超過1024

有時使用者會需要一種在一個範圍內查詢某個文件,比如查詢某一時間段內的所有文件,此時,Lucene提供了一種名為RangeQuery的類來滿足這種需求。

RangeQuery表示在某範圍內的搜尋條件,實現從一個開始詞條到一個結束詞條的搜尋功能,在查詢時“開始詞條”和“結束詞條”可以被包含在內也可以不被包含在內。它的具體用法如下:

RangeQuery query = new RangeQuery(begin, end, included);

在引數列表中,最後一個boolean值表示是否包含邊界條件本身,即當其為TRUE時,表示包含邊界值,用字元可以表示為“[begin TO end]”;當其為FALSE時,表示不包含邊界值,用字元可以表示為“{begin TO end}”。

下面通過程式碼11.6介紹RangeQuery使用的方法。

程式碼11.6  RangeQueryTest.java

package ch11;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.Term;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.RangeQuery;

public class RangeQueryTest {

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

         //生成文件物件,下同

         Document doc1 = new Document();

         //新增“time”欄位中的內容,下同

         doc1.add(Field.Text("time", "200001"));

         //新增“title”欄位中的內容,下同

         doc1.add(Field.Keyword("title", "doc1"));

         Document doc2 = new Document();

         doc2.add(Field.Text("time", "200002"));

         doc2.add(Field.Keyword("title", "doc2"));

         Document doc3 = new Document();

         doc3.add(Field.Text("time", "200003"));

         doc3.add(Field.Keyword("title", "doc3"));

         Document doc4 = new Document();

         doc4.add(Field.Text("time", "200004"));

         doc4.add(Field.Keyword("title", "doc4"));

         Document doc5 = new Document();

         doc5.add(Field.Text("time", "200005"));

         doc5.add(Field.Keyword("title", "doc5"));

         //生成索引書寫器

         IndexWriter writer = new IndexWriter("c://index", new StandardAnalyzer(), true);

         //設定為混合索引格式

          writer.setUseCompoundFile(true);

         //將文件物件新增到索引中

         writer.addDocument(doc1);

         writer.addDocument(doc2);

         writer.addDocument(doc3);

         writer.addDocument(doc4);

         writer.addDocument(doc5);

         //關閉索引

         writer.close();

         //生成索引搜尋器

         IndexSearcher searcher = new IndexSearcher("c://index");

         //構造詞條

         Term beginTime = new Term("time","200001");

         Term endTime = new Term("time","200005");

         //用於儲存檢索結果

         Hits hits = null;

         //生成RangeQuery物件,初始化為null

         RangeQuery query = null;

         //構造RangeQuery物件,檢索條件中不包含邊界值

         query = new RangeQuery(beginTime, endTime, false);

         //開始檢索,並返回檢索結果

         hits = searcher.search(query);

         //輸出檢索結果的相關資訊

         printResult(hits, "200001200005的文件,不包括200001200005");

         //再構造一個RangeQuery物件,檢索條件中包含邊界值

         query = new RangeQuery(beginTime, endTime, true);

         //開始第二次檢索

         hits = searcher.search(query);

         //輸出檢索結果的相關資訊

         printResult(hits, "200001200005的文件,包括200001200005");

     }

     public static void printResult(Hits hits, String key) throws Exception

         {System.out.println("查詢 /"" + key + "/" :");

         if (hits != null) {

             if (hits.length() == 0) {

                 System.out.println("沒有找到任何結果");

             } else {

                 System.out.print("找到");

                 for (int i = 0; i < hits.length(); i++) {

                     Document d = hits.doc(i);

                     String dname = d.get("title");

                     System.out.print(dname + "   " );

                 }

                 System.out.println();

                 System.out.println();

             }

         }

     }

}

在上述程式碼中首先構造了兩個Term詞條,然後構造了一個RangeQuery物件。在初始化RangeQuery物件的時候,使用構造的兩個Term詞條作為RangeQuery建構函式的引數。前面已經說過,RangeQuery的建構函式中的兩個引數分別稱為“開始詞條”和“結束詞條”,它的含義也就是查詢介於這兩者之間的所有Document

構建的Document的“time”欄位值均介於200001200005之間,其檢索結果如圖11-11所示。

11-11  RangeQuery測試結果

從圖11-11中可以看出,在程式碼11.6中使用RangeQuery共進行了兩次檢索,第一次的檢索條件中不包括邊界值,第二次的檢索條件中包括邊界值。

從程式碼11.6和圖11-11中可以看出,第1次使用FALSE引數構造的RangeQuery物件不包括2個邊界值,因此只返回3Document,而第2次使用TRUE引數構造的RangeQuery則包括2個邊界值,因此將5Document全部返回了。

PrefixQuery就是使用字首來進行查詢的。通常情況下,首先定義一個詞條Term。該詞條包含要查詢的欄位名以及關鍵字的字首,然後通過該詞條構造一個PrefixQuery物件,就可以進行字首查找了。

下面以程式碼11.7為例來介紹使用PrefixQuery進行檢索的執行過程。

程式碼11.7  PrefixQueryTest.java

package ch11;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.Term;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.PrefixQuery;

import org.apache.lucene.search.RangeQuery;

public class PrefixQueryTest {

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

         //生成Document物件,下同

         Document doc1 = new Document();

         //新增“name”欄位的內容,下同

         doc1.add(Field.Text("name", "David"));

         //新增“title”欄位的內容,下同

         doc1.add(Field.Keyword("title", "doc1"));

         Document doc2 = new Document();

         doc2.add(Field.Text("name", "Darwen"));

         doc2.add(Field.Keyword("title", "doc2"));

         Document doc3 = new Document();

         doc3.add(Field.Text("name", "Smith"));

         doc3.add(Field.Keyword("title", "doc3"));

         Document doc4 = new Document();

         doc4.add(Field.Text("name", "Smart"));

         doc4.add(Field.Keyword("title", "doc4"));

         //生成索引書寫器

         IndexWriter writer = new IndexWriter("c://index",

                 new StandardAnalyzer(), true);

         //設定為混合索引模式

         writer.setUseCompoundFile(true);

         //依次將文件新增到索引中

         writer.addDocument(doc1);

         writer.addDocument(doc2);

         writer.addDocument(doc3);

         writer.addDocument(doc4);

         //關閉索引書寫器

         writer.close();

         //生成索引搜尋器物件

         IndexSearcher searcher = new IndexSearcher("c://index");

         //構造詞條

         Term pre1 = new Term("name", "Da");

         Term pre2 = new Term("name", "da");

         Term pre3 = new Term("name", "sm");

         //用於儲存檢索結果

         Hits hits = null;

         //生成PrefixQuery型別的物件,初始化為null

         PrefixQuery query = null;

         query = new PrefixQuery(pre1);

         //開始第一次檢索,並返回檢索結果

         hits = searcher.search(query);

         //輸出相應的檢索結果

         printResult(hits, "字首為'Da'的文件");

         query = new PrefixQuery(pre2);

         //開始第二次檢索,並返回檢索結果

         hits = searcher.search(query);

         //輸出相應的檢索結果

         printResult(hits, "字首為'da'的文件");

         query = new PrefixQuery(pre3);

         //開始第二次檢索,並返回檢索結果

         hits = searcher.search(query);

         //輸出相應的檢索結果

         printResult(hits, "字首為'sm'的文件");

     }

     public static void printResult(Hits hits, String key) throws Exception

         {System.out.println("查詢 /"" + key + "/" :");

         if (hits != null) {

             if (hits.length() == 0) {

                 System.out.println("沒有找到任何結果");

                 System.out.println();

             } else {

                 System.out.print("找到");

                 for (int i = 0; i < hits.length(); i++) {

                     //取得文件

                     Document d = hits.doc(i);

                     //取得“title”欄位的內容

                     String dname = d.get("title");

                     System.out.print(dname + "   ");

                 }

                 System.out.println();

                 System.out.println();

             }

         }

     }

}

在上述程式碼中,首先構造了4個不同的Document。每個Document都有一個名為“name”的欄位,其中儲存了人物的名稱。然後,程式碼構建了3個不同的詞條,分別為“Da”、“da”和“sm”,可以看到,它們正好都是“name”欄位中關鍵字的字首。

程式碼的執行結果如圖11-12所示。

文字框:
圖11-12  PrefixQuery測試結果從圖11-12中可以看出,使用PrefixQuery共進行了3次檢索,關鍵字分別為“Da”、“da”和“sm”,返回的檢索結果情況在圖中已經有明確的說明。不過,如果使用“Da”作為關鍵字會沒有任何的檢索結果,而使用“da”就有檢索結果,這個問題將在後面作詳細介紹。

從程式碼11.7和圖11-12中可以看出,“da”字首和“sm”字首都順利地找到了它們所在的文件,可是為什麼與文件中關鍵字大小寫一致的“Da”卻沒有找到呢?這是因為Lucene的標準分析器在進行分詞過濾時將所有的關鍵字一律轉成了小寫,所以才會出現這樣的結果。這也是開發者應當引起注意的地方。

除了普通的TermQuery外,Lucene還提供了一種Phrase查 詢的功能。使用者在搜尋引擎中進行搜尋時,常常查詢的並非是一個簡單的單詞,很有可能是幾個不同的關鍵字。這些關鍵字之間要麼是緊密相聯,成為一個精確的短 語,要麼是可能在這幾個關鍵字之間還插有其他無關的關鍵字。此時,使用者希望將它們找出來。不過很顯然,從評分的角度看,這些關鍵字之間擁有與查詢內容無關 短語所在的文件的分值一般會較低一些。

PhraseQuery正是Lucene所提供的滿足上述需求的一種Query物件。它的add方法可以讓使用者往其內部新增關鍵字,在新增完畢後,使用者還可以通過setSlop()方法來設定一個稱之為“坡度”的變數來確定關鍵字之間是否允許、允許多少個無關詞彙的存在。

下面以程式碼11.8為例對PhraseQuery進行介紹。

程式碼11.8  PhraseQueryTest.java

package ch11;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.Term;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.PhraseQuery;

import org.apache.lucene.search.PrefixQuery;

public class PhraseQueryTest {

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

         //生成Document物件

         Document doc1 = new Document();

         //新增“content”欄位的內容

         doc1.add(Field.Text("content", "david mary smith robert"));

         //新增“title”欄位的內容

         doc1.add(Field.Keyword("title", "doc1"));

         //生成索引書寫器

         IndexWriter writer = new IndexWriter("c://index",

                 new StandardAnalyzer(), true);

         //設定為混合索引格式

         writer.setUseCompoundFile(true);

         //將文件新增到索引中

         writer.addDocument(doc1);

         //關閉索引

         writer.close();

         //生成索引搜尋器

         IndexSearcher searcher = new IndexSearcher("c://index");

         //構造詞條

         Term word1 = new Term("content", "david");

         Term wor