Lucene教程


一:簡單的示例
   1.1:生成索引
     1.1.1:Field.Store和Field.Index
     1.1.2:為數字生成索引
     1.1.3:為索引加權
     1.1.4:為日期生成索引
  1.2:查詢
    1.2.1:介紹IndexReader
1.3:刪除
    1.3.1:還原刪除的文檔
    1.3.2:清空回收站時面的數據
  1.4:更新

 

 

前言:本教程用於Lucene3.5,Maven地址為

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>3.5.0</version>
        </dependency>

 

一:簡單的示例

我就不介紹Lucene了,想來看這篇博客的人,都知道Lucene是什么。直接給出生成索引,和查詢的示例

1.1:生成索引

生成索引的代碼如下:

    /**
     * 創建索引
     */
    public void index(){
        IndexWriter writer = null;
        try {
            //1、創建Derictory
//        Directory directory = new RAMDirectory();//這個方法是建立在內存中的索引
            Directory directory = FSDirectory.open(new File("G:\\TestLucene\\index"));//這個方法是建立在磁盤上面的索引
//        2、創建IndexWriter,用完后要關閉
            IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35));
            writer = new IndexWriter(directory,config);
            //3、創建Document對象
            Document document = null;
            File fl = new File("G:\\TestLucene\\file");
            //4、為Document添加Field
            for(File file : fl.listFiles()){
                document = new Document();
       document.add(
new Field("content",new FileReader(file))); //把文件名存放到硬盤中,不作分詞 document.add(new Field("fileName",file.getName(),Field.Store.YES, Field.Index.ANALYZED.NOT_ANALYZED)); //把絕對路徑放到硬盤中,不作分詞 document.add(new Field("path", file.getAbsolutePath(), Field.Store.YES, Field.Index.NOT_ANALYZED)); } //5、通過IndexWriter添加文檔到索引中 writer.addDocument(document); } catch (IOException e) { e.printStackTrace(); } finally { if(null != writer){ try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } }

1.1.1:Field.Store和Field.Index

這里說明一個Field.Index和Field.Store

                //Field.Store.YES或者NO部分
                // 如果為YES,代表着是否要把這個域中的內容完全存儲到文件中,方便進行還原
                //如果為NO,代表着不把這個域的內容存儲到文件,但是可以被索引,但是這些內容不可被還原

                //Field.Index.ANALYZED:進行分詞和索引,適用於標題,內容等
                //Field.Index.NOT_ANALYZED:進行索引,但是不進行分詞。精准的數據不分詞,像id,身份證號,姓名等不分詞,用於精確搜索
                //Field.Index.ANALYZED_NOT_NORMS:進行分詞但是不存儲norm信息,這個norms中包括了索引的時間和權值等信息
                //Field.Index.NOT_ANALYZED_NOT_NORMS:既不進行分詞,也不存儲norms信息
                //Field.Index.NO:完全不進行索引

 

 

1.1.2:為數字生成索引

看過Field構造方法的人可能知道,這里面並沒有對數字索引添加方法,那么會有人說,把數字轉換成字符串?額。數字在索引中處理方式與字符串不同,我們可以使用一個新的對象

 

            //搜索content中包含有着like的
            TermQuery termQuery = new TermQuery(new Term("content","like"));
                //給數字加索引要用另一個對象
                document.add(new NumericField("attachs").setIntValue(attachs[i]));

 

                //給數字加索引要用另一個對象
                //查看源碼會發現,這個構造函數默認是不存儲,但是會進行索引
                document.add(new NumericField("attachs").setIntValue(attachs[i]));
                //通過這個構造方法,可以把其修改為存儲,最后的boolean參數代表着是否索引
                document.add(new NumericField("attachs", Field.Store.YES,true).setIntValue(attachs[i]));

 

這里使用一個新的字段,NumericField

 

 

1.1.3:為索引加權

大家看到搜索引擎的排序,就肯定能猜到,搜索引擎是按照了一定的要求,對查詢的結果進行了排序,這里介紹一個簡單的加權排序方法,后面會深入研究

//加權
document.setBoost(2.1f);

 

注意:權重越大,排序越前

 

 

 

1.1.4:為日期生成索引

既然數字有專門的NumericField,那么給日期生成索引,是不是也有DateField呢?其實是沒有的,那怎么辦?

但是我們都忽略了一件事,日期其實也是一個long類型的數字

 document.add(new NumericField("attachs", Field.Store.YES,true).setLongValue(new Date().getTime()));

 

這不就行了嗎?

 

1.2:查詢

這里演示根據已生成的索引,來查詢

代碼如下:

 /**
     * 搜索
     */
    public void searcher(){
        try {
            //1、創建Directory
            Directory directory = FSDirectory.open(new File("G:\\TestLucene\\index"));

            //2、創建IndexReader,需要關閉
            IndexReader reader = IndexReader.open(directory);

            //3、根據IndexReader創建IndexSearcher
            IndexSearcher searcher = new IndexSearcher(reader);

            //4、創建索引的Query
            //第二個參數代表着要搜索的域
            QueryParser parser = new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35));
            //表示搜索content中包含java的文檔
            Query query = parser.parse("朱小傑");
      
//5、根據searcher搜索並返回TopDocs // 表示返回前面10條 TopDocs topDocs = searcher.search(query,10); //6、根據TopDocs獲取ScoreDoc對象 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for(ScoreDoc sd : scoreDocs){ //7、根據Searcher和ScordDoc對象獲取具體的Document對象 //獲取這個文檔的id int doc = sd.doc; Document document = searcher.doc(doc); //8、根據Document對象獲取需要的值 System.out.println("【找到】" + document.get("fileName") + " " + document.get("path") + " .." + document.get("content")); } reader.close(); } catch (IOException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } }

 

1.2.1:介紹IndexReader

IndexReader顧名思義,它是用來讀取索引的信息的,下面來演示一些它的用法

(1)獲取文檔的數量

//存儲的文檔數量,也就是document對象的數量,刪除索引后,這個數值會減少
System.out.println("存儲的文檔數量: " + reader.numDocs());

(2)獲取文檔的總量

//存儲過的文檔的最大數量,刪除索引后,數量不會減少
//此時刪除的文件並不會完全刪除,它存在回收站里面
System.out.println("文檔存儲的總存儲量: " + reader.maxDoc());

 (3)獲取已刪除文檔的數量

System.out.println("刪除文檔的數量: " + reader.numDeletedDocs());

 

1.3:刪除

下面給出刪除的代碼

    /**
     * 刪除索引
     */
    public void delete(){
        try {
            IndexWriter writer = null;
            writer = new IndexWriter(directory,new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35)));

            //刪除全部的索引
            //writer.deleteAll();

            //參數可以為一個查詢的Query,也可以為一個Term,它是一個精確的值,代表着把id為1的給刪除掉
            writer.deleteDocuments(new Term("id","1"));


            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意,這里的刪除,並不是真的刪除。執行完之后,可以在索引的目錄里面看到多了一個.del的文件,那是一個類似回收站的文件,在回收站中的文件是可以進行還原的

 

1.3.1:還原刪除的文檔

 

之前有說到,刪除並沒有作真正的刪除,而是把這個文件放到了類似回收站的位置中,下面來使用代碼來進行還原已刪除的文件

 

    /**
     * 刪除索引並不是完全刪除,它是有着一個回收站的功能
     * 上面的delete刪除了一個索引,這里進行恢復
     */
    public void recovery(){
        try {
            //這一步很重要,因為默認打開的reader是只讀的,所以這里要通過構造方法,把它的readonly設置為false,否則會拋出異常
            IndexReader reader = IndexReader.open(directory,false);
            //還原所有已刪除的數據
            reader.undeleteAll();

            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

注意:上面的構造方法和以往不同,后面多了一個boolean值,這個值,如果不寫,默認是true,代表着只讀,那么如果在這種情況下進行還原,是會拋出異常的。這里將其設置為false,也就是把只讀設置為了false,這樣就可以還原了。

 

1.3.2:清空回收站里面的數據

上面說完從回收站里面還原數據,那么回收站怎么清空掉呢?下面給出代碼:

    /**
     * 清空回收站里面的數據
     */
    public void clearRecovery(){
        try {
            IndexWriter writer = new IndexWriter(directory,new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35)));

            writer.forceMergeDeletes();
            //代表着是否等待當前操作完成后,再清空回收站里面的數據
            writer.forceMergeDeletes(true);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

這里面是有着兩個重載的方法,其中一個是立即刪除,一個是等待當前操作完成后,再刪除

 

 

1.4:更新

更新一個索引的代碼如下:

    /**
     * 更新數據
     */
    public void update(){
        try {
            //注意,Lucene其實並沒有更新的操作,它的實際原理是先刪除,再添加
            IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35));
            IndexWriter writer = new IndexWriter(directory,config);

            Document document = new Document();
            document.add(new Field("id","1", Field.Store.YES, Field.Index.NOT_ANALYZED));
            writer.updateDocument(new Term("id","1"),document);

            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

值得注意的是,這里的更新,並不是在原有的記錄里面更新,而是先把該記錄刪除,然后增加新的記錄,所以在查看已刪除的文檔數量里面會發出多出一條記錄,同樣的,在文檔總量里面,也會增加一條記錄

 

二:IndexReader的設計

2.1:設計單例的IndexReader

為什么要設計一個單例的IndexReader呢?大家可以試着去想像,假如說一個硬盤上面的索引,隨着日期的增加,那么它的索引也就越來越多,當我打開一個IndexReader的時候,肯定是要讀取索引里面的信息的,如果索引文件過多的話,那么肯定是會造成創建這個對象的時間及性能上面的消耗,所以IndexReader很有必要設計成單例的。

 

2.2:當索引的內容發生改變時,單例的IndexReader對象不會改變的問題

由上面的單例IndexReader,這里又有着一個新的問題,那就是在一個項目中,存在一個單例的IndexReader的時候,雖然可以大大提升性能,但是也有一個問題。IndexReader對象里面的索引內容,是在這個對象被創建的時候生成的,也只有在那個時候,IndexReader才能讀取到索引目錄里面的數據。

問題就是,當索引內容添加,或者刪除過后,IndexReader的對象不會發生改變!!

 

下面來研究創建IndexReader的方法:

//根據一個Directory創建一個IndexReader
IndexReader reader = IndexReader.open(directory);

上面的這個創建IndexReader的方法,將會讀取索引中所有的數據,首先消耗性能是肯定的。

 

 

其實還有一個創建IndexReader的方法,如下:

//這種創建IndexReader的方法,就是把老的IndexReader對象傳進去,然后會判斷索引的內容是否會發生改變,如果索引內容發生改變,則會創建一個新的對象,如果索引的內容沒有發生改變,則會返回空
IndexReader ir = IndexReader.openIfChanged(reader);

 

 

 這是一個新的方法,通過這個方法,就可以知道是否需要產生新的IndexReader方法,下面來演示一下IndexReader的設計

public class CustomerIndexReader {
    static {
        try {
           directory = FSDirectory.open(new File("d:/index"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    private static IndexReader reader = null;
    private static Directory directory = null;

    public CustomerIndexReader(){

    }
    
    public IndexReader getIndexReader(){
        if(directory == null){
            synchronized (this){
                if(directory == null){
                    try {
                        reader = IndexReader.open(directory);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }else{
            try {
                IndexReader ir = IndexReader.openIfChanged(reader);
                if(ir != null){
                    //如果這個對象不為空,則代表着索引發生了改變
                    reader = ir;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return reader;
    }
}

 

 

其實IndexReader也有刪除文檔的方法。而且它可以保證IndexReader的數據是最新的數據。也就是reader.deleteDocument()

 

三:查詢的方式

3.1:精確查詢

何為精確查詢,精確查詢就相當於數據庫的=號,也就是查詢的字符,與索引中字符必須完全一致,才能匹配到

 

    public void searchers(){
        try {
            Directory directory = FSDirectory.open(new File("d:/index"));
            IndexReader reader = IndexReader.open(directory);
            IndexSearcher searcher = new IndexSearcher(reader);
            //這個是精確查詢
            Query query = new TermQuery(new Term("name","大牛"));

            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

如上面的代碼,會在name域里面找名字為“大牛”的結果,但是如果搜索“大”,或者“牛”,就找不到結果,因為TermQuery是精確查詢

 

 

3.2:字符串的范圍搜索

說完精確搜索,下面介紹一下范圍搜索。范圍搜索,也就是指在一定區間范圍內查詢,下面給出代碼的示例。

    public void searcher1(){
        try {
            Directory directory = FSDirectory.open(new File("d:/index"));
            IndexReader reader = IndexReader.open(directory);
            IndexSearcher searcher = new IndexSearcher(reader);
            //這是珍上范圍搜索,意思是搜索id域中,最低為1,最高為10,后面的兩個boolean的參數分別代表着,是否包好最低值與最高值
            //但是數字類型是查不出來的,也就是NumericField來存儲field的類型,使用TermRangeQuery是查不出來的,需要使用NumericRangeQuery
            Query query = new TermRangeQuery("id","1","10",true,true);

            //查詢名字以a 開頭,到以f開頭的
          //  Query query = new TermRangeQuery("name","a","f",true,true);
      reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意:TermRangeQuery無法查詢數字的結果,也就是使用NumericField來存儲的索引,但是可以查詢"1","2"字符串類型的數字。

 

 

 

3.3:數字的范圍搜索

上面說了字符串的范圍搜索,而且還特意強掉了,數字不能用TermRangeQuery,那么如果數字的范圍搜索,要怎么做呢?可以使用NumericRangeQuery,下面給出代碼:

    public void searcher2(){
        try {
            Directory directory = FSDirectory.open(new File("d:/index"));
            IndexReader reader = IndexReader.open(directory);
            IndexSearcher searcher = new IndexSearcher(reader);

            //這里是int整數的查詢方法,其實,還有float,long,double等方式,也都是通過NumericRangeQuery這個類
           // NumericRangeQuery.newDoubleRange(..);  這是Double類型的
            //NumericRangeQuery.newFloatRange(...);  這是Float類型的
            //NumericRangeQuery.newLongRange(...);   這是Long類型的
        //這里的意思是查詢age域中,1歲到100歲的,其中,包含1歲和100歲的
Query query = NumericRangeQuery.newIntRange("age",1,100,true,true); searcher.close(); reader.close; } catch (IOException e) { e.printStackTrace(); } }

 

3.4:前綴搜索

前綴搜索,就是對於一個域中的前綴進行匹配,當然,它也會匹配分詞后的前綴

Query query = new PrefixQuery(new Term("name","劉"));

上面的代碼,會找出所有name中姓劉的數據。

注意:如果內容中進行了分詞,那將會查找每一個分詞中的以此字符開頭的數據。

 

 

3.5:通配符搜索

通配符大家應該聽說過的,那就是*代表任何字符,?代表一個字符

Query query = new WildcardQuery(new Term("content","*a?"));

 

 

3.6:連接多個條件的查詢

有的時候,查詢一個復雜的數據,一個搜索條件,可能不滿足結果,那么就可以使用BooleanQuery

            //這個query下面可以add任何多個查詢條件
            BooleanQuery query = new BooleanQuery();
            //名字一定是張三
            query.add(new TermQuery(new Term("name","張三")), BooleanClause.Occur.MUST);
            //名族一定不是漢族
            query.add(new TermQuery(new Term("nation","漢")), BooleanClause.Occur.MUST_NOT);
      //可以出現,也可以不出現 query.add(
new WildcardQuery(new Term("content","a")), BooleanClause.Occur.SHOULD);

 

BooleanQuery就是Query的擴展類,這個類可以增加任意多個查詢條件,並且通過Occur枚舉過定義,查詢條件的必要性

 

 

 

3.7:短語間隔搜索

就是查詢一定區間的字符。可能這句話說不明白,我們用代碼來說明:

假如我有下面的一段字符

I love lucene very much

那么我現在的目的是,我忘了中間的單詞是什么了,我只記得開頭為I,結尾為much,那要怎么做呢?

            PhraseQuery query = new PhraseQuery();
            //第一個結果,注意I會變成小寫
            query.add(new Term("content","i"));
            //代表着中間相隔3個單詞
            query.setSlop(3);
            //第二個結果
            query.add(new Term("content","nuch"));

 

注意:大寫的開頭,會被轉換成小寫哦,但是這種方法開銷很大,盡量少用

 

 

 

3.8:模糊查詢

這里要先說明一下,模糊查詢與通配符查詢是有區別的。模糊查詢是代表着允許有着一定的錯別字

這里來進行說明一下,假如我有這樣的一些name屬性

jane  mike  kangkang 

當我寫出下面的代碼的時候

            //通過這個,肯定是可以找到mkie的結果的
            FuzzyQuery query1 = new FuzzyQuery(new Term("name","mike"));
            //這里我把i寫成了a,但是也是可以查到mike的
            FuzzyQuery query2 = new FuzzyQuery(new Term("name","make"));

上面的代碼代表着,FuzzyQuery,允許有着一定的錯別字

那么可以控制查詢字符的錯別字嗎?

答案是可以的,如下面的代碼:

//通過第2個float參數調整相似度,值越低,代表相似度越低,容錯率越高
FuzzyQuery query3 = new FuzzyQuery(new Term("name","make"),0.5f,0);

 

它會有着一定的容錯率

 

 

3.9:QueryParser的使用

在剛開始的示例中,就使用過QueryParser的這個對象,現在就來重點的說明一下。

QueryParser它支持一定的查詢表達式,什么是查詢表達式呢?下面用代碼來演示一下

            //創建一個默認搜索域為content的parser
            QueryParser parser = new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35));
            //改變字符串的默認操作符,下面改成AND
            //parser.setDefaultOperator(QueryParser.Operator.AND);
            //開啟第一個字符的通配符的匹配,lucene默認是關閉的,因為效率太低
            parser.setAllowLeadingWildcard(true);

            //搜索content中包含like的
            Query query = parser.parse("like");

            //搜索有dog或者cat的,空格默認就是OR
            query = parser.parse("dog cat");

            //改變搜索域為name,搜索其中的jie
            query = parser.parse("name:jie");

            //使用通配符*和?來進行匹配
            query = parser.parse("name:j*");
            //通配符默認是不能放在首位的,因為其效率太低,lucene默認關閉了,上面已經開始,所以不會拋異常
            query = parser.parse("name:*e");

            //搜索name中沒有dog,默認域content中有eat的條件
            query = parser.parse("- name:dog + eat");

            //匹配一個區間,TO必須是大寫,這個區間是開區間,這個是字符的1,數字的不能
            query = parser.parse("id:[1 TO 3]");
            //這個是閉區間,只會匹配到2,這個是字符的1,數字的不能
            query = parser.parse("id:{1 TO 3}");

            //默認域中是dog或者cat,但是age是11的
            query = parser.parse("(dog OR cat) AND age:11");

            //匹配兩個相連的字符串,這里不會被分割,代表着默認域中,這兩個字符串相連的才會被搜索出來
            query = parser.parse("\"hello world\"");

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM