在前面概要的了解了lucene的內容下面就深入一下lucene的各個模塊。這里我們主要深入一下lucene的索引,就是如何構建索引的過程及概念。
lucene與關系型數據庫
從兩個角度比較一下吧,一個是從索引方面,一個是模糊查詢,其實歸為一類的化就是全文檢索的對比。
1、索引的對比
對比項 | 全文檢索庫(Lucene) | 關系型數據庫 |
核心功能 | 以文本檢索為主,插入、刪除、修改比較麻煩,適合於大文本塊的查詢。 | 插入、刪除、修改十分方便,有專門的SQL命令,但對於大文本塊類型的檢索效率較低。 |
庫 | 與數據庫類似,都可以建多個庫,而且各個庫的存儲位置可以不同。 | 可以建多個庫。一般每個庫都有控制文件和數據文件等,比較復雜。 |
表 | 沒有嚴格的表的概念,Lucene的表只是由入庫時的定義字段松散構成 | 有嚴格的表結構,有主鍵,有字段類型等 |
記錄 | 由於沒有嚴格的表的概念,所以記錄體現為一個對象,記錄對應的類是Document。 | Record,與表結構對應。 |
字段 | 字段類型只有文本和日期兩種,字段一般不支持運算,更無函數功能,字段對應的類是Field類。 | 字段類型豐富,功能強大。 |
查詢結果集 | 在Lucene里表示查詢結果集的類是Hits,如hits(doc1,doc2,doc3……) | 在JDBC中使用Resultset |
2、模糊查詢的對比
對比項 | Lucene全文檢索 | 數據庫模糊查詢 |
索引 | 將數據源中的數據——建立倒排索引,速度較快 | 無法使用數據庫索引,需要遍歷所有記錄進行模糊匹配,所以查詢速度有多個數量級的下降 |
匹配效果 | 通過詞元匹配,通過語言分析接口進行關鍵詡拆分,能夠實現對中文的支持 | 由於是模糊查詢,匹配不精確,可能查出無關信息或漏查信息 |
匹配度 | 有匹配度算法,將匹配度比較高的結果排在前面 | 沒有匹配度算法,一個關鍵詞在記錄中出現多少次結果都是一樣的 |
結果輸出 | 通過特別的算法,將匹配度最高的頭100條結果輸出,結果集是緩沖式的小批量讀取的,系統開銷較小 | 返回所有的結果集,在匹配條目非常多的時候需要大量的內存存放這些臨時結果集,系統開銷大 |
可定制性 | 通過API接口可定制出符合檢索排序需要的排序規則 | 不可定制 |
適用情況 | 高負載的模糊查詢應用,索引資料量比較大,速度要求比較快,匹配度要求比較高的情況 | 使用率低,模糊匹配規則的簡單或者需要模糊查詢的資料量少的情況 |
索引創建的過程
索引創建的過程可以分為將原始文檔轉換成文本、分析文本、將分析好的文本保存至索引中這么幾個過程。
圖:lucene構建索引過程
1、提取文本的過程可以使用我們自己的處理方式也可以使用開源框架Tika來處理。
2、分析文檔這個過程很重要,當我們建立起文檔和域之后,就可以使用IndexWriter對象的addDocument方法將數據傳遞給Lucene進行索引操作了。
3、當輸入數據分析完畢后,就可以將分析的結果寫入到索引文件中了。Lucene將輸入數據以一種倒排索引的數據結構進行存儲。
什么是倒排索引
倒排索引源於實際應用中需要根據屬性的值來查找記錄。這種索引表中的每一項都包括一個屬性值和具有該屬性值的各記錄的地址。由於不是由記錄來確定屬性值, 而是由屬性值來確定記錄的位置,因而稱為倒排索引(inverted index)。帶有倒排索引的文件我們稱為倒排索引文件,簡稱倒排文件(inverted file)。
也就是說,倒排索引並不是回答“一個文檔中包含哪些單詞、詞組”,而是經過優化后回答“哪個文檔中包含這個單詞、詞組”。就是更符合我們的要求和習慣的一種做法。
基本索引操作
向索引添加文檔
向索引中添加文檔的方法主要有:
- addDocument(Document)——使用默認的分析器添加文檔
- addDocument(Document,Analyzer)——使用指定的分析器添加文檔和語匯單元化操作
我們在內存中先建立一下索引,然后用測試方法測試一下添加索引的動作。程序結構如下:

1 protected String[] ids = { "1", "2" }; 2 protected String[] unindexed = { "Netherlands", "Italy" }; 3 protected String[] unstored = { "Amsterdam has lots of bridges", 4 "Venice has lots of canals" }; 5 protected String[] text = { "Amsterdam", "Venice" }; 6 7 private Directory directory; 8 9 protected void setUp() throws Exception { // 1 10 directory = new RAMDirectory(); 11 12 IndexWriter writer = getWriter(); // 2 13 14 for (int i = 0; i < ids.length; i++) { // 3 15 Document doc = new Document(); 16 doc.add(new Field("id", ids[i], Field.Store.YES, 17 Field.Index.NOT_ANALYZED)); 18 doc.add(new Field("country", unindexed[i], Field.Store.YES, 19 Field.Index.NO)); 20 doc.add(new Field("contents", unstored[i], Field.Store.NO, 21 Field.Index.ANALYZED)); 22 doc.add(new Field("city", text[i], Field.Store.YES, 23 Field.Index.ANALYZED)); 24 writer.addDocument(doc); 25 } 26 writer.close(); 27 } 28 29 private IndexWriter getWriter() throws IOException { // 2 30 return new IndexWriter(directory, new WhitespaceAnalyzer(), // 2 31 IndexWriter.MaxFieldLength.UNLIMITED); // 2 32 } 33 34 protected int getHitCount(String fieldName, String searchString) 35 throws IOException { 36 IndexSearcher searcher = new IndexSearcher(directory); // 4 37 Term t = new Term(fieldName, searchString); 38 Query query = new TermQuery(t); // 5 39 int hitCount = TestUtil.hitCount(searcher, query); // 6 40 searcher.close(); 41 return hitCount; 42 } 43 44 public void testIndexWriter() throws IOException { 45 IndexWriter writer = getWriter(); 46 assertEquals(ids.length, writer.numDocs()); // 7 47 writer.close(); 48 } 49 50 public void testIndexReader() throws IOException { 51 IndexReader reader = IndexReader.open(directory); 52 assertEquals(ids.length, reader.maxDoc()); // 8 53 assertEquals(ids.length, reader.numDocs()); // 8 54 reader.close(); 55 }
其中testIndexWriter()方法用來核對寫入的文檔數,也就是說我們向索引中加入的Document的數量。
上面程序中ids的數量是2,所以這里assertEquals()得出的結果也應該是2,兩個結果相同,程序正常執行。
然后我們可以看測試程序testIndexReader()方法是用來得到索引對象並且讀出Document的數量。
刪除索引中的文檔
刪除索引中的文檔主要有下面幾個方法:
- deleteDocuments(Term)——刪除指定包含項的文檔
- deleteDocuments(Term[ ])——刪除包含項數組中的所有文檔
- deleteDocuments(Query)——刪除匹配查詢語句的所有文檔
- deleteDocuments(Query[ ])——刪除匹配查詢數組中的所有文檔
- deleteAll()——刪除索引中的所有文檔
這兩個方法是確定刪除文檔的程序,程序結構如下:

1 public void testDeleteBeforeOptimize() throws IOException { 2 IndexWriter writer = getWriter(); 3 assertEquals(2, writer.numDocs()); // A 4 writer.deleteDocuments(new Term("id", "1")); // B 5 writer.commit(); 6 assertTrue(writer.hasDeletions()); // 1 7 assertEquals(2, writer.maxDoc()); // 2 8 assertEquals(1, writer.numDocs()); // 2 9 writer.close(); 10 } 11 12 public void testDeleteAfterOptimize() throws IOException { 13 IndexWriter writer = getWriter(); 14 assertEquals(2, writer.numDocs()); 15 writer.deleteDocuments(new Term("id", "1")); 16 writer.optimize(); // 3 17 writer.commit(); 18 assertFalse(writer.hasDeletions()); 19 assertEquals(1, writer.maxDoc()); // C 20 assertEquals(1, writer.numDocs()); // C 21 writer.close(); 22 }
這兩個測試程序都是刪除已經構建好的索引並且測試得到的結果。
更新索引中的文檔
其實在lucene中的更新操作就是先刪除原來的舊的文檔然后加入新的文檔,也就是如果我們想更新某個文檔中的域的變化,那么就需要先刪除原來的Document,然后再新加入新的Document。
程序結構如下:

1 public void testUpdate() throws IOException { 2 3 assertEquals(1, getHitCount("city", "Amsterdam")); 4 5 IndexWriter writer = getWriter(); 6 7 Document doc = new Document(); //A 8 doc.add(new Field("id", "1", 9 Field.Store.YES, 10 Field.Index.NOT_ANALYZED)); //A 11 doc.add(new Field("country", "Netherlands", 12 Field.Store.YES, 13 Field.Index.NO)); //A 14 doc.add(new Field("contents", 15 "Den Haag has a lot of museums", 16 Field.Store.NO, 17 Field.Index.ANALYZED)); //A 18 doc.add(new Field("city", "Den Haag", 19 Field.Store.YES, 20 Field.Index.ANALYZED)); //A 21 22 writer.updateDocument(new Term("id", "1"), //B 23 doc); //B 24 writer.close(); 25 26 assertEquals(0, getHitCount("city", "Amsterdam"));//C 27 assertEquals(1, getHitCount("city", "Haag")); //D 28 }
在這個程序里就是先建立新的Document,然后更新舊文檔,最后確認新文檔被索引。
Field(域)
域索引選項
這個主要是控制域文本是否可被搜索,如何搜索,具體的幾個選項如下:
- Index.ANALYZED——分析指定的文本,就是我們在域中指定的選項,比如文章的標題、正文、摘要等。
- Index.NOT_ANALYZED——這個比較適合於精確匹配,比如我們要搜索的是一個固定的電話號碼,有點類似於SQL中的select * from 表 where phoneNum='指定值'。
- Index.NO——對應的域值不被索引。
域存儲選項
用來確定是否需要存儲域的真實值,也就是說索引的信息需不需要恢復。兩個可選值如下:
- Store.YES——存儲的值是原始值,也就是說根據索引能夠得到原始的值,適合不太大的域值,太大的話會很消耗內存。
- Store.NO——不存儲原始值,也就是不能恢復,通常用來索引大塊的域。
多值域
比如你的文檔有一個域表示作者名字,但有時該文檔的作者數不止一個。這時候就需要我們向域中寫入不同的值,就像這樣:

1 Document doc = new Document(); 2 for (String author : authors) { 3 doc.add(new Field("author", author, Field.Store.YES, 4 Field.Index.ANALYZED)); 5 }
這種方式的處理是被鼓勵和接受的。
加權
如果我們有這樣一個需求,就是對索引的文檔分出主次或者區分出權限比重,那么使用加權操作就會非常容易的實現這個功能。
給文檔加權
如果我們為公司設計搜索程序來索引和搜索公司的E-Mail情況,該程序要求在進行搜索結果排序時,公司員工的E-Mail比其它E-Mail有更重要的位置,那么就會用到加權操作。
設置不同的加權因子,程序結構如下:

1 public void docBoostMethod() throws IOException { 2 3 Directory dir = new RAMDirectory(); 4 IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED); 5 6 // START 7 Document doc = new Document(); 8 String senderEmail = getSenderEmail(); 9 String senderName = getSenderName(); 10 String subject = getSubject(); 11 String body = getBody(); 12 doc.add(new Field("senderEmail", senderEmail, 13 Field.Store.YES, 14 Field.Index.NOT_ANALYZED)); 15 doc.add(new Field("senderName", senderName, 16 Field.Store.YES, 17 Field.Index.ANALYZED)); 18 doc.add(new Field("subject", subject, 19 Field.Store.YES, 20 Field.Index.ANALYZED)); 21 doc.add(new Field("body", body, 22 Field.Store.NO, 23 Field.Index.ANALYZED)); 24 String lowerDomain = getSenderDomain().toLowerCase(); 25 if (isImportant(lowerDomain)) { 26 doc.setBoost(1.5F); //1 27 } else if (isUnimportant(lowerDomain)) { 28 doc.setBoost(0.1F); //2 29 } 30 writer.addDocument(doc); 31 // END 32 writer.close(); 33 34 /* 35 #1 Good domain boost factor: 1.5 36 #2 Bad domain boost factor: 0.1 37 */ 38 }
對公司內部的人員郵件加索引時,默認加權因子設置為1.5,其它的設置為0.1,好了,在搜索的期間,這些權值高的就會被先搜索出來。
給域加權
還是上面的例子,如何能使郵件的主題比作者更重要呢,那么就會用到域加權操作。給文檔加權會默認給文檔中的所有域都進行加權,如果想給域加權,我們需要使用Field的setBoost(float)方法,程序結構如下:

1 public void fieldBoostMethod() throws IOException { 2 3 String senderName = getSenderName(); 4 String subject = getSubject(); 5 6 // START 7 Field subjectField = new Field("subject", subject, 8 Field.Store.YES, 9 Field.Index.ANALYZED); 10 subjectField.setBoost(1.2F); 11 // END 12 }
索引數字、日期和時間
為什么要單獨出來說這個呢,因為有的時候你可能有這樣的需求,比如你要搜索的是價格信息,需要的是一個精度的搜索,有時候你要搜索一個長度的范圍或者接收信息的日期等信息,這些信息通常都是默認被索引成數字,也就是說你可能不能找到你想要匹配的結果,這時候就需要做一些單獨的的處理,在我們加入Field的時候。
索引數字的程序結構:

1 public void numberField() { 2 Document doc = new Document(); 3 // START 4 doc.add(new NumericField("price").setDoubleValue(19.99)); 5 // END 6 }
索引日期和時間的程序結構:

1 public void numberTimestamp() { 2 Document doc = new Document(); 3 // START 4 doc.add(new NumericField("timestamp") 5 .setLongValue(new Date().getTime())); 6 // END 7 8 // START 9 doc.add(new NumericField("day") 10 .setIntValue((int) (new Date().getTime()/24/3600))); 11 // END 12 13 Date date = new Date(); 14 // START 15 Calendar cal = Calendar.getInstance(); 16 cal.setTime(date); 17 doc.add(new NumericField("dayOfMonth") 18 .setIntValue(cal.get(Calendar.DAY_OF_MONTH))); 19 // END 20 }
優化索引
首先要弄清楚一點,優化索引的目的是為了提高搜索速度而不是為了提高索引速度。
如何優化呢,這里簡單的做一下整理:
- 確認你在使用Lucene的最新版本
- 盡量使用本地文件系統
- 使用更快的硬件設備,特別是更快的IO設備
- 加大你的機器內存容量,給Java虛擬機分配更多的內存
- 在程序中使用一個唯一的IndexSearch實例
- 當測試搜索速度時,忽略第一次查詢時間
- 在搜索之前調用optimize優化你的索引
- 考慮使用filters
當然這里只是列出了一部分的優化手段,具體的情況還需要根據具體的環境來分析,畢竟滿足需求才是最重要的。
索引的鎖機制
1、在lucene中,鎖機制是與並發性相關的一個主題,在同一時刻只允許單一進程的所有代碼段中,lucene都創建了基於文件的鎖,以此來避免誤用 lucene的api造成對索引的損壞。每個索引都有自身的鎖文件集。鎖文件放在計算機的臨時目錄中,這個目錄由java的java.io.tmpdir 中的系統屬性所指定。
2、(1)IndexReader的isLocked(Directory)-這個方法可以判斷參數中指定的索引是否已經被上鎖。
(2)IndexReader的unlock(Directory)-手動解鎖,使用它有危險性,因為lucene加鎖有其理由。