前面兩篇博文介紹了Lucene的原理思想和架構,這篇博文就寫一下實際開發中如何使用Lucene,下面先用一個最簡單的例子描述一下。
1. 建立索引
爲了簡單起見,我們下面爲一些字符串創建內存索引:
StandardAnalyzer analyzer = newStandardAnalyzer(Version.LUCENE_40);
Directory index = newRAMDirectory();
IndexWriterConfig config = newIndexWriterConfig(Version.LUCENE_40, analyzer);
IndexWriter w = newIndexWriter(index, config);
addDoc(w,"Lucene in Action","193398817");
addDoc(w,"Lucene for Dummies","55320055Z");
addDoc(w,"Managing Gigabytes","55063554A");
addDoc(w,"The Art of Computer Science","9900333X");
w.close();
addDoc()方法把文檔(譯者注:這裏的文檔是Lucene中的Document類的實例)添加到索引中。
private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
Document doc = newDocument();
doc.add(newTextField("title", title, Field.Store.YES));
doc.add(newStringField("isbn", isbn, Field.Store.YES));
w.addDocument(doc);
}
注意,對於需要分詞的內容我們使用TextField,對於像id這樣不需要分詞的內容我們使用StringField。
2.搜索請求
我們從標準輸入(stdin)中讀入搜索請求,然後對它進行解析,最後創建一個Lucene中的Query對象.
String querystr = args.length > 0? args[0] : "lucene";
Query q = newQueryParser(Version.LUCENE_40,"title", analyzer).parse(querystr);
3.搜索
我們創建一個Searcher對象並且使用上面創建的Query對象來進行搜索,匹配到的前10個結果封裝在TopScoreDocCollector對象裏返回。
int hitsPerPage = 10;
IndexReader reader = IndexReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true);
searcher.search(q, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;
4.展示
現在我們得到了搜索結果,我們需要想用戶展示它。
System.out.println("Found " + hits.length + " hits.");
for(int i=0; i<hits.length; ++i) {
int docId = hits[i].doc;
Document d = searcher.doc(docId);
System.out.println((i + 1) + ". " + d.get("isbn") + "\t"+ d.get("title"));
}
這裏是這個小應用的完整代碼。下載HelloLucene.java。
public class HelloLucene {
public static void main(String[] args) throws IOException, ParseException {
// 0. Specify the analyzer for tokenizing text.
// The same analyzer should be used for indexing and searching
StandardAnalyzer analyzer = new StandardAnalyzer(Version.LUCENE_40);
// 1. create the index
Directory index = newRAMDirectory();
IndexWriterConfig config = newIndexWriterConfig(Version.LUCENE_40, analyzer);
IndexWriter w = newIndexWriter(index, config);
addDoc(w,"Lucene in Action","193398817");
addDoc(w,"Lucene for Dummies","55320055Z");
addDoc(w,"Managing Gigabytes","55063554A");
addDoc(w,"The Art of Computer Science","9900333X");
w.close();
// 2. query
String querystr = args.length > 0? args[0] : "lucene";
// the "title" arg specifies the default field to use
// when no field is explicitly specified in the query.
Query q = newQueryParser(Version.LUCENE_40,"title", analyzer).parse(querystr);
// 3. search
inthitsPerPage = 10;
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher = newIndexSearcher(reader);
TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true);
searcher.search(q, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;
// 4. display results
System.out.println("Found " + hits.length + " hits.");
for(int i=0; i<hits.length; ++i) {
int docId = hits[i].doc;
Document d = searcher.doc(docId);
System.out.println((i + 1) + ". " + d.get("isbn") + "\t"+ d.get("title"));
}
// reader can only be closed when there
// is no need to access the documents any more.
reader.close();
}
private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
Document doc = new Document();
doc.add(new TextField("title", title, Field.Store.YES));
// use a string field for isbn because we don't want it tokenized
doc.add(new StringField("isbn", isbn, Field.Store.YES));
w.addDocument(doc);
}
通過上面你的Demo例子,我想你一定對Lucene有一定了解了,下面來總結一下Lucene的使用。
|
一、環境
需要導入lucene.jar包(在lucene.apache.org下載)
二、基本概念
1.Lucene的工作流程:
(1) 使用IndexWriter,在指定的目錄建立索引的文件
(2) 將需要檢索的數據轉換位Document的Filed對象,然後將Document用IndexWriter添加倒索引的文件中
(3) 處理索引信息,關閉IndexWriter流
(4) 創建搜索的Query
(5) 給IndexSearcher
2.Lucene的字段類型
Lucene有四種不同的字段類型:Keyword,UnIndexed,UnStored和Text,用於指定建立最佳索引。
(1)Keyword字段是指不需要分析器解析但需要被編入索引並保存到索引中的部分。JavaSourceCodeIndexer類使用該字段來保存導入類的聲明。
(2)UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的將其值保存到索引中。由於我們一般要存儲文件的位置但又很少用文件名作爲關鍵字來搜索,所以用該字段來索引Java文件名。
(3)UnStored字段和UnIndexed字段相反。該類型的Field要被分析並編入索引,但其值不會被保存到索引中。由於存儲方法的全部源代碼需要大量的空間。所以用UnStored字段來存儲被索引的方法源代碼。可以直接從Java源文件中取出方法的源代碼,這樣作可以控制我們的索引的大小。
(4)Text字段在索引過程中是要被分析、索引並保存的。類名是作爲Text字段來保存。下表展示了JavaSourceCodeIndexer類使用Field字段的一般情況。
3.基本概念(與傳統表的對比)
Lucene概念
|
傳統概念
|
備註
|
IndexWriter
|
table
|
|
Document
|
一條記錄
|
|
Field
|
每個字段
|
分爲可被索引的,可切分的,不可被切分的,不可被索引的幾種組合類型
|
Hits
|
RecoreSet
|
結果集
|
IndexWriter提供了一些參數可供設置,列表如下:
(1).mergeFactory: 控制index的大小和頻率,默認值:10,兩個作用:1.一個段有多少document,2.多少個段合成一個大段
(2).maxMergeDocs:限制一個段中的document數目,默認值:Integer.MAX_VALUE
(3).minMergeDocs:緩存在內存中的document數目,超過他以後會寫入到磁盤,默認值:10
(4).maxFieldLength:一個Field中最大Term數目,超過部分忽略,不會index到field中,所以自然也就搜索不到,默認值:1000
這些參數的的詳細說明比較複雜:mergeFactor有雙重作用
(1)設置每mergeFactor個document寫入一個段,比如每10個document寫入一個段
(2)設置每mergeFacotr個小段合併到一個大段,比如10個document的時候合併爲1小段,以後有10個小段以後合併到一個大段,有10個大段以後再合併,實際的document數目會是mergeFactor的指數
簡單的來說mergeFactor 越大,系統會用更多的內存,更少磁盤處理,如果要打批量的作index,那麼把mergeFactor設置大沒錯, mergeFactor 小了以後, index數目也會增多,searhing的效率會降低,但是mergeFactor增大一點一點,內存消耗會增大很多(指數關係),所以要留意不要”out of memory”
把maxMergeDocs設置小,可以強制讓達到一定數量的document寫爲一個段,這樣可以抵消部分mergeFactor的作用.
minMergeDocs相當於設置一個小的cache,第一個這個數目的document會留在內存裏面,不寫入磁盤。這些參數同樣是沒有最佳值的,必須根據實際情況一點點調整。
maxFieldLength可以在任何時刻設置,設置後,接下來的index的Field會按照新的length截取,之前已經index的部分不會改變。可以設置爲Integer.MAX_VALUE
4.幾種查詢方式
查詢方式
|
說明
|
TermQuery
|
條件查詢 例如:TermQuery tquery=new TermQuery(new Term("name","jerry")); name:字段名 jerry:要搜索的字符串
|
MultiTermQuery
|
多個字段進行同一關鍵字的查詢 Query query= null; Query =MultiFieldQueryParser.parse("我",new String[] {"title","content"},analyzer); Searcher searcher=new IndexSearcher(indexFilePath); Hits hits=searcher.search(query);
|
BooleanQuery
|
例如:BooleanQuery bquery=new BooleanQuery(); bquery.add(query,true,false); bquery.add(mquery,true,false); bquery.add(tquery,true,false); Searcher searcher=new IndexSearcher(indexFilePath); Hits hits=searcher.search(bquery);
|
WildcardQuery
|
語義查詢(通配符查詢) 例:Query query= new WildcardQuery(new Term("sender","*davy*"));
|
PhraseQuery
|
短語查詢
|
PrefixQuery
|
前綴查詢
|
PhrasePrefixQuery
|
短語前綴查詢
|
FuzzyQuery
|
模糊查詢
|
RangeQuery
|
範圍查詢
|
SpanQuery
|
範圍查詢
|
在全文檢索時建議大家先採用語義時的搜索,先搜索出有意義的內容,之後再進行模糊之類的搜索
(1)聯合兩個索引查詢,已解決:
IndexSearcher[] searchers = new IndexSearcher[2];
searchers[0] = new IndexSearcher(m_indexpath);
searchers[1] = new IndexSearcher(m_outindexpath);
MultiSearcher multiSearcher = new MultiSearcher(searchers);v
(2)還有個進行多條件搜索 and 與 or 的操作————
用 MultiFieldQueryParser
建議重新封裝
MultiFieldQueryParser.Parser(p[],d[],f[],analyer) 成or 與 and操作合一
或者
BooleanQuery m_BooleanQuery = new BooleanQuery();
Query query = QueryParser.Parse(m_SearchText, "INSTRUMENT_NAME", analyzer);
Query query2 = QueryParser.Parse(m_SearchText2, "INSTRUMENT_NAME2", analyzer);
m_BooleanQuery.Add(query, true, false);
m_BooleanQuery.Add(query2, true, false);
(3)複合查詢(多種查詢條件的綜合查詢)
Query query=MultiFieldQueryParser.parse("索引”,new String[] {"title","content"},analyzer);
Searcher searcher=new IndexSearcher(indexFilePath);
Hits hits=searcher.search(query);
for (int i = 0; i < hits.length(); i++) {
System.out.println(hits.doc(i).get("name"));
}
5.爲查詢優化索引(index)
Indexwriter.optimize()方法可以爲查詢優化索引(index),之前提到的參數調優是爲indexing過程本身優化,而這裏是爲查詢優化,優化主要是減少index文件數,這樣讓查詢的時候少打開文件,優化過程中,lucene會拷貝舊的index再合併,合併完成以後刪除舊的index,所以在此期間,磁盤佔用增加, IO符合也會增加,在優化完成瞬間,磁盤佔用會是優化前的2倍,在optimize過程中可以同時作search。
4.org.apache.lucene.document.Field
即上文所說的“字段”,它是Document的片段section。
Field的構造函數:
Field(String name, String string, boolean store, boolean index, boolean token)。
Indexed:如果字段是Indexed的,表示這個字段是可檢索的。
Stored:如果字段是Stored的,表示這個字段的值可以從檢索結果中得到。
Tokenized:如果一個字段是Tokenized的,表示它是有經過Analyzer轉變後成爲一個tokens序列,在這個轉變過程tokenization中, Analyzer提取出需要進行索引的文本,而剔除一些冗餘的詞句(例如:a,the,they等,詳見 org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和 org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引時候的.
類型
|
Analyzed
|
Indexed
|
Stored
|
說明
|
Field.Keyword(String,String/Date)
|
N
|
Y
|
Y
|
這個Field用來儲存會直接用來檢索的比如(編號,姓名,日期等)
|
Field.UnIndexed(String,String)
|
N
|
N
|
Y
|
不會用來檢索的信息,但是檢索後需要顯示的,比如,硬件序列號,文檔的url地址
|
Field.UnStored(String,String)
|
Y
|
Y
|
N
|
大段文本內容,會用來檢索,但是檢索後不需要從index中取內容,可以根據url去load真實的內容
|
Field.Text(String,String)
|
Y
|
Y
|
Y
|
|
|
5.Lucene 的檢索結果排序
Lucene的排序主要是對org.apache.lucene.search.Sort的使用。Sort可以直接根據字段Field生成,也可以根據標準的SortField生成,但是作爲Sort的字段,必須符合以下的條件:唯一值以及Indexed。可以對Integers, Floats, Strings三種類型排序。
對整數型的ID檢索結果排序只要進行以下的簡單操作:
Sort sort = new Sort("id");
Hits hits = searcher.search(query, sort);
用戶還可以根據自己定義更加複雜的排序,詳細請參考
6.分詞器(分析器)
Lucene使用分析器來處理被索引的文本。在將其存入索引之前,分析器用於將文本標記化、摘錄有關的單詞、丟棄共有的單詞、處理派生詞(把派生詞還原到詞根形式,意思是把bowling、bowler和bowls還原爲bowl)和完成其它要做的處理。Lucene提供的通用分析器是:
SimpleAnalyzer:用字符串標記一組單詞並且轉化爲小寫字母。
StandardAnalyzer:用字符串標記一組單詞,可識別縮寫詞、email地址、主機名稱等等。並丟棄基於英語的stop words (a, an, the, to)等、處理派生詞。
ChineseAnalyzer.class,它是一個單字分析法,它把句子中的詞全部分成一個一個的字符,以單個字爲單位存儲。
CJKAnalyzer.class,它是雙字分析法,它把中文以雙字爲單位拆分得到結果,從而建立詞條。當然這些得到的雙字詞中會有很多不符合中文語義單位的雙字被送進索引。
十、需要注意的問題:
1 .IndexWriter在添加新的document後,需要重新建立Index,則需要調用writer.optimize();方法
2. Lucene沒有update索引的方法,需要刪除後重新建立,參考remove方法
3 .用IndexReader刪除Document後,需要重新用IndexWriter進行整理,否則無法在進行搜索(不知道是不是我設置問題)
4.Lucene先在內存中進行索引操作,並根據一定的批量進行文件的寫入。這個批次的間隔越大,文件的寫入次數越少,但佔用內存會很多。反之佔用內存少,但文件IO操作頻繁,索引速度會很慢。在IndexWriter中有一個MERGE_FACTOR參數可以幫助你在構造索引器後根據應用環境的情況充分利用內存減少文件的操作。根據我的使用經驗:缺省Indexer是每20條記錄索引後寫入一次,每將MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
5.併發操作Lucene
(1)所有隻讀操作都可以併發
(2)在index被修改期間,所有隻讀操作都可以併發
(3)對index修改操作不能併發,一個index只能被一個線程佔用
(4)ndex的優化,合併,添加都是修改操作
(5)但需要注意的是,在創建搜索的時候用:
searcher = new IndexSearcher(IndexReader.open("E:\\lucene\\test4\\index"));
searcher.close();
這時候是不能關閉searcher的.
如果想讓searcher能關閉,就不要用IndexReader了:
searcher = new IndexSearcher("E:\\lucene\\test4\\index");
6.Locking機制
lucence內部使用文件來locking,默認的locking文件放在java.io.tmpdir,可以通過-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock兩個文件,lock文件用來防止並行操作index,如果並行操作, lucene會拋出異常,可以通過設置-DdisableLuceneLocks=true來禁止locking,這樣做一般來說很危險,除非你有操作系統或者物理級別的只讀保證,比如把index文件刻盤到CDROM上。
十一、2.0中新增特性
1.新增類: org.apache.lucene.index.IndexModifier ,它合併了 IndexWriter 和 IndexReader,好處是我們可以增加和刪除文檔的時候不同擔心 synchronisation/locking 的問題了。
2.增加對 contrib/highlighter 的 NullFragmenter , 這對全文本加亮很有用。
3.增加了新類 MatchAllDocsQuery 用來匹配所有文檔。
4.. 增加 ParallelReader,這個一種IndexReader 他合併多個單獨的索引到一個單獨的虛擬索引上。
5.增加 Hits.iterator() 方法和相應的 HitIterator 和 Hit 對象。
他提供了對 Hits對象標準的 java.util.Iterator 疊代操作。
每個iterator's next() 方法返回一個 Hit 對象。
6. 在 term vectors 中增加了位置和偏移信息。(Grant Ingersoll & Christoph)
7. 增加了一個新的 DateTools 。允許用戶格式化日期到一種更可讀的格式,以便於更好的適應索引。
8. 增加了對壓縮字段存儲的支持。
實例:
1.判斷索引文件是否存在:
public static boolean indexExist(String indexDir){
return IndexReader.indexExists(indexDir);
}
private IndexWriter getWriter(String indexFilePath) throws Exception {
boolean append=true;
File file=new File(indexFilePath+File.separator+"segments");
if(file.exists())
append=false;
return new IndexWriter(indexFilePath,analyzer,append);
}
2.刪除索引
public static void deleteIndex(Term aTerm, String indexDir) {
List aList = new ArrayList();
aList.add(aTerm);
deleteIndex(aList, indexDir);
}
public static void deleteIndex(List terms, String indexDir) {
if (null == terms) {
return;
}
if(!indexExist(indexDir)) { return; }
IndexReader reader = null;
try {
reader = IndexReader.open(indexDir);
for (int i = 0; i < terms.size(); i++){
Term aTerm = (Term) terms.get(i);
if (null != aTerm){
reader.delete(aTerm);
}
}
} catch (IOException e){
LogMan.warn("Error in Delete Index", e);
} finally {
try{
if (null != reader){
reader.close();
}
}catch (IOException e){
LogMan.warn("Close reader Error");
}
}
}
刪除索引需要一個條件,類似數據庫中的字段條件,例如刪除一條新聞的代碼如下:
public static void deleteNewsInfoIndex(int nid){
Term aTerm = new Term("nid", String.valueOf(nid));
deleteIndex(aTerm,indexDir);
}
public class LuceneSearch {
public static void main(String[] args) throws Exception{
LuceneSearch test = new LuceneSearch();
Hits h = null;
h = test.search("顯示 ");
test.printResult(h);
h = test.search("jy");
test.printResult(h);
h = test.search("djy");
test.printResult(h);
h = test.search("料不");
test.printResult(h);
h = test.search("人");
test.printResult(h);
}
public LuceneSearch(){
try{
searcher = new IndexSearcher(IndexReader.open("E:\\lucene\\test4\\index"));
}catch(Exception e){
e.printStackTrace();
}
}
//聲明一個IndexSearcher對象
private IndexSearcher searcher = null;
//聲明一個Query對象
private Query query = null;
ChineseAnalyzer analyzer = new ChineseAnalyzer();
Highlighter highlighter = null;
public final Hits search(String keyword){
System.out.println("正在檢索關鍵字:"+keyword);
try{
Date start = new Date();
/***** 一個關鍵字,對一個字段進行查詢 *****/
QueryParser qp = new QueryParser("content",analyzer);
query = qp.parse(keyword);
// Hits hits = searcher.search(query);
/***** 模糊查詢 *****/
// Term term = new Term("content",keyword);
// FuzzyQuery fq = new FuzzyQuery(term);
// Hits hits = searcher.search(fq);
/***** 一個關鍵字,在兩個字段中查詢 *****/
/*
* 1.BooleanClause.Occur[]的三種類型:
* MUST : + and
* MUST_NOT : - not
* SHOULD : or
* 2.下面查詢的意思是:content中必須包含該關鍵字,而title有沒有都無所謂
* 3.下面的這個查詢中,Occur[]的長度必須和Fields[]的長度一致。每個限制條件對應一個字段
*/
// BooleanClause.Occur[] flags = new BooleanClause.Occur[]{BooleanClause.Occur.SHOULD,BooleanClause.Occur.MUST};
// query=MultiFieldQueryParser.parse(keyword,new String[]{"title","content"},flags,analyzer);
/***** 兩個(多個)關鍵字對兩個(多個)字段進行查詢,默認匹配規則 *****/
/*
* 1.關鍵字的個數必須和字段的個數相等
* 2.由於沒有指定匹配規定,默認爲"SHOULD"
* 因此,下面查詢的意思是:"title"中含有keyword1 或 "content"含有keyword2.
* 在此例中,把keyword1和keyword2相同
*/
// query=MultiFieldQueryParser.parse(new String[]{keyword,keyword},new String[]{"title","content"},analyzer);
/***** 兩個(多個)關鍵字對兩個(多個)字段進行查詢,手工指定匹配規則 *****/
/*
* 1.必須 關鍵字的個數 == 字段名的個數 == 匹配規則的個數
* 2.下面查詢的意思是:"title"必須不含有keyword1,並且"content"中必須含有keyword2
*/
// BooleanClause.Occur[] flags = new BooleanClause.Occur[]{BooleanClause.Occur.MUST_NOT,BooleanClause.Occur.MUST};
// query=MultiFieldQueryParser.parse(new String[]{keyword,keyword},new String[]{"title","content"},flags,analyzer);
/***** 對日期型字段進行查詢 *****/
/***** 對數字範圍進行查詢 *****/
/*
* 1.兩個條件必須是同一個字段
* 2.前面一個條件必須比後面一個條件小,否則找不到數據
* 3.new RangeQuery中的第三個參數,表示是否包含"="
* true: >= 或 <=
* false: > 或 <
* 4.找出 55>=id>=53 or 60>=id>=57:
*/
// Term lowerTerm1 = new Term("id","53");
// Term upperTerm1 = new Term("id","55");
// RangeQuery rq1 = new RangeQuery(lowerTerm1,upperTerm1,true);
//
// Term lowerTerm2 = new Term("id","57");
// Term upperTerm2 = new Term("id","60");
// RangeQuery rq2 = new RangeQuery(lowerTerm2,upperTerm2,true);
//
// BooleanQuery bq = new BooleanQuery();
// bq.add(rq1,BooleanClause.Occur.SHOULD);
// bq.add(rq2,BooleanClause.Occur.SHOULD);
//手工拼範圍
// query = QueryParser.Parse("{200004 TO 200206}", "pubmonth", new SimpleAnalyzer());
// Lucene用[] 和{}分別表示包含和不包含.
//String temp = "startDate:["+nextWeek[0]+" TO "+nextWeek[1]+"] ";
// temp = temp + " OR endDate:["+nextWeek[0]+" TO "+nextWeek[1]+"]";
// Query query1 = qp.parse(temp);
// Hits hits = searcher.search(bq);
/***** 排序 *****/
/*
* 1.被排序的字段必須被索引過(Indexecd),在索引時不能 用 Field.Index.TOKENIZED
* (用UN_TOKENIZED可以正常實現.用NO時查詢正常,但排序不能正常設置升降序)
* 2.SortField類型
* SCORE、DOC、AUTO、STRING、INT、FLOAT、CUSTOM
* 此類型主要是根據字段的類型選擇
* 3.SortField的第三個參數代表是否是降序
* true:降序 false:升序
*/
// Sort sort = new Sort(new SortField[]{new SortField("id", SortField.INT, true)});
// Hits hits = searcher.search(query,sort);
/*
* 按日期排序
*/
// Sort sort = new Sort(new SortField[]{new SortField("createTime", SortField.INT, false)});
/***** 過濾器 ******/
// QueryParser qp1 = new QueryParser("content",analyzer);
// Query fquery = qp1.parse("我");
//
// BooleanQuery bqf = new BooleanQuery();
// bqf.add(fquery,BooleanClause.Occur.SHOULD);
//
// QueryFilter qf = new QueryFilter(bqf);
Hits hits = searcher.search(query);
Date end = new Date();
System.out.println("檢索完成,用時"+(end.getTime()-start.getTime())+"毫秒");
return hits;
}catch(Exception e){
e.printStackTrace();
return null;
}
}
public void printResult(Hits h){
if(h.length() == 0){
System.out.println("對不起,沒有找到您要的結果.");
}else{
for(int i = 0; i < h.length(); i++){
try{
Document doc = h.doc(i);
System.out.println("結果"+(i+1)+":"+doc.get("id")+" createTime:"+doc.get("createTime")+" title:"+doc.get("title")+" content:"+doc.get("content"));
//System.out.println(doc.get("path"));
}catch(Exception e){
e.printStackTrace();
}
}
}
System.out.println("--------------------------------------");
}
}