一:簡單的示例 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\"");