1,
有時對于一個Document來說,有一些Field會被頻繁地操作,而另一些Field則不會。這時可以將頻繁操作的Field和其他Field分開存
放,而在搜索時同時檢索這兩部分Field而提取出一個完整的Document。 這要求兩個索引包含的Document的數量必須相同。
在創建索引的時候,可以同時創建多個IndexWriter,將一個Document根據需要拆分成多個包含部分Field的Document,并將這些Document分別添加到不同的索引。
而在搜索時,則必須借助ParallelReader類來整合。
Directory dir1=FSDirectory.getDirectory(new File(INDEX_DIR1),false);
Directory dir2=FSDirectory.getDirectory(new File(INDEX_DIR2),false);
ParallelReader preader=new ParallelReader();
preader.add(IndexReader.open(dir1));
preader.add(IndexReader.open(dir2));
IndexSearcher searcher=new IndexSearcher(preader);
之后的操作和一般的搜索相同。
2, Query的子類. 下面的幾個搜索在各種不同要求的場合,都會用到. 需要大家仔細研讀!
Query query1 = new TermQuery(new Term(FieldValue, "name1")); // 詞語搜索
Query query2 = new WildcardQuery(new Term(FieldName, "name*")); // 通配符
Query query3 = new PrefixQuery(new Term(FieldName, "name1")); // 字段搜索 Field:Keyword,自動在結尾添加 *
Query query4 = new RangeQuery(new Term(FieldNumber,
NumberTools.LongToString(11L)), new Term(FieldNumber,
NumberTools.LongToString(13L)), true); // 范圍搜索
Query query5 = new FilteredQuery(query, filter); // 帶過濾條件的搜索
Query query6 =new MatchAllDocsQuery(... // 用來匹配所有文檔
Query query7 = new FuzzyQuery (...模糊搜索
Query query8 = new RegexQuery (.. 正則搜索
Query query9 = new SpanRegexQuery(...)。 同上, 正則表達式的查詢:
Query query9 = new SpanQuery 的子類嵌套其他SpanQuery 增加了 rewrite方法
Query query10 =new DisjunctionMaxQuery () ..類,提供了針對某個短語的最大score。這一點對多字段的搜索非常有用
Query query11 = new ConstantScoreQuery 類它包裝了一個 filter produces a score
equal to the query boost for every matching document.
BooleanQuery query12= new BooleanQuery();
booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);
booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);
//這個是為了聯合多個查詢而做的Query類. BooleanQuery增加了最小的匹配短語。見:BooleanQuery.setMinimumNumberShouldMatch().
PhraseQuery
你可能對中日關系比較感興趣,想查找‘中’和‘日’挨得比較近(5個字的距離內)的文章,超過這個距離的不予考慮,你可以:
PhraseQuery query 13= new PhraseQuery();
query.setSlop(5);
query.add(new Term("content ", “中”));
query.add(new Term(“content”, “日”));
PhraseQuery對于短語的順序是不管的,這點在查詢時除了提高命中率外,也會對性能產生很大的影響, 利用SpanNearQuery可以對短語的順序進行控制,提高性能
BooleanQuery query12= new SpanNearQuery 可以對短語的順序進行控制,提高性能
3, 索引文本文件
如果你想把純文本文件索引起來,而不想自己將它們讀入字符串創建field,你可以用下面的代碼創建field:
Field field = new Field("content", new FileReader(file));
這里的file就是該文本文件。該構造函數實際上是讀去文件內容,并對其進行索引,但不存儲
4, 如何刪除索引
lucene提供了兩種從索引中刪除document的方法,一種是
void deleteDocument(int docNum)
這種方法是根據document在索引中的編號來刪除,每個document加進索引后都會有個唯一編號,所以根據編號刪除是一種精確刪除,但是這個編號是索引的內部結構,一般我們不會知道某個文件的編號到底是幾,所以用處不大。另一種是
void deleteDocuments(Term term)
這種方法實際上是首先根據參數term執行一個搜索操作,然后把搜索到的結果批量刪除了。我們可以通過這個方法提供一個嚴格的查詢條件,達到刪除指定document的目的。
下面給出一個例子:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(field, key);
reader.deleteDocuments(term);
reader.close();
5, 如何更新索引
lucene并沒有提供專門的索引更新方法,我們需要先將相應的document刪除,然后再將新的document加入索引。例如:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(“title”, “lucene introduction”);
reader.deleteDocuments(term);
reader.close();
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content", "lucene is funny", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
但是在1.9RC1中說明:
新增類: org.apache.lucene.index.IndexModifier ,它合并了 IndexWriter 和
IndexReader,好處是我們可以增加和刪除文檔的時候不同擔心 synchronisation/locking 的問題了。
6, filer類.使用 Filter 對搜索結果進行過濾,可以獲得更小范圍內更精確的結果。 有人說: 注意它執行的是預處理,而不是對查詢結果進行過濾,所以使用filter的代價是很大的,它可能會使一次查詢耗時提高一百倍
ISOLatin1AccentFilter ,用 ISO Latin 1 字符集中的unaccented類字符替代 accented 類字符
DateFilter 日期過濾器
RangeFileter ,比 DateFilter 更加通用,實用
LengthFilter 類, 已經從 contrib 放到了 core 代碼里。從 stream 中去掉太長和太短的單詞 StopFilter 類, 增加了對處理stop words 的忽略大小寫處理
7,本條是一個使用過濾的說明:
過濾
使用 Filter 對搜索結果進行過濾,可以獲得更小范圍內更精確的結果。
舉個例子,我們搜索上架時間在 2005-10-1 到 2005-10-30 之間的商品。
對于日期時間,我們需要轉換一下才能添加到索引庫,同時還必須是索引字段。
// index
document.Add(FieldDate, DateField.DateToString(date), Field.Store.YES, Field.Index.UN_TOKENIZED);
//...
// search
Filter filter = new DateFilter(FieldDate, DateTime.Parse("2005-10-1"), DateTime.Parse("2005-10-30"));
Hits hits = searcher.Search(query, filter);
除了日期時間,還可以使用整數。比如搜索價格在 100 ~ 200 之間的商品。
Lucene.Net NumberTools 對于數字進行了補位處理,如果需要使用浮點數可以自己參考源碼進行。
// index
document.Add(new Field(FieldNumber, NumberTools.LongToString((long)price), Field.Store.YES, Field.Index.UN_TOKENIZED));
//...
// search
Filter filter = new RangeFilter(FieldNumber, NumberTools.LongToString(100L), NumberTools.LongToString(200L), true, true);
Hits hits = searcher.Search(query, filter);
使用 Query 作為過濾條件。
QueryFilter filter = new QueryFilter(QueryParser.Parse("name2", FieldValue, analyzer));
我們還可以使用 FilteredQuery 進行多條件過濾。
Filter filter = new DateFilter(FieldDate, DateTime.Parse("2005-10-10"), DateTime.Parse("2005-10-15"));
Filter filter2 = new RangeFilter(FieldNumber, NumberTools.LongToString(11L), NumberTools.LongToString(13L), true, true);
Query query = QueryParser.Parse("name*", FieldName, analyzer);
query = new FilteredQuery(query, filter);
query = new FilteredQuery(query, filter2);
IndexSearcher searcher = new IndexSearcher(reader);
Hits hits = searcher.Search(query);
8, Sort
有時你想要一個排好序的結果集,就像SQL語句的“order by”,lucene能做到:通過Sort。
Sort sort = new Sort(“time”); //相當于SQL的“order by time”
Sort sort = new Sort(“time”, true); // 相當于SQL的“order by time desc”
下面是一個完整的例子:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new IndexSearcher(dir);
QueryParser parser = new QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("title:lucene content:lucene";
RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true);
Sort sort = new Sort(“time”);
Hits hits = is.search(query, filter, sort);
for (int i = 0; i < hits.length(); i++)
{
Document doc = hits.doc(i);
System.out.println(doc.get("title");
}
is.close();
9, 性能優化
一直到這里,我們還是在討論怎么樣使lucene跑起來,完成指定任務。利用前面說的也確實能完成大部分功能。但是測試表明lucene的性能并不是很
好,在大數據量大并發的條件下甚至會有半分鐘返回的情況。另外大數據量的數據初始化建立索引也是一個十分耗時的過程。那么如何提高lucene的性能呢?
下面從優化創建索引性能和優化搜索性能兩方面介紹。
9.1 優化創建索引性能
這方面的優化途徑比較有限,IndexWriter提供了一些接口可以控制建立索引的操作,另外我們可以先將索引寫入RAMDirectory,再批量寫
入FSDirectory,不管怎樣,目的都是盡量少的文件IO,因為創建索引的最大瓶頸在于磁盤IO。另外選擇一個較好的分析器也能提高一些性能。
9.1.1 通過設置IndexWriter的參數優化索引建立
setMaxBufferedDocs(int maxBufferedDocs)
控制寫入一個新的segment前內存中保存的document的數目,設置較大的數目可以加快建索引速度,默認為10。
setMaxMergeDocs(int maxMergeDocs)
控制一個segment中可以保存的最大document數目,值較小有利于追加索引的速度,默認Integer.MAX_VALUE,無需修改。
setMergeFactor(int mergeFactor)
控制多個segment合并的頻率,值較大時建立索引速度較快,默認是10,可以在建立索引時設置為100。
9.1.2 通過RAMDirectory緩寫提高性能
我們可以先把索引寫入RAMDirectory,達到一定數量時再批量寫進FSDirectory,減少磁盤IO次數。
FSDirectory fsDir = FSDirectory.getDirectory("/data/index", true);
RAMDirectory ramDir = new RAMDirectory();
IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);
IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
while (there are documents to index)
{
... create Document ...
ramWriter.addDocument(doc);
if (condition for flushing memory to disk has been met)
{
fsWriter.addIndexes(new Directory[] { ramDir });
ramWriter.close();
ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
}
}
9.1.3 選擇較好的分析器
這個優化主要是對磁盤空間的優化,可以將索引文件減小將近一半,相同測試數據下由600M減少到380M。但是對時間并沒有什么幫助,甚至會需要更長時
間,因為較好的分析器需要匹配詞庫,會消耗更多cpu,測試數據用StandardAnalyzer耗時133分鐘;用MMAnalyzer耗時150分
鐘。
9.2 優化搜索性能
雖然建立索引的操作非常耗時,但是那畢竟只在最初創建時才需要,平時只是少量的維護操作,更何況這些可以放到一個后臺進程處理,并不影響用戶搜索。我們創建索引的目的就是給用戶搜索,所以搜索的性能才是我們最關心的。下面就來探討一下如何提高搜索性能。
9.2.1 將索引放入內存
這是一個最直觀的想法,因為內存比磁盤快很多。Lucene提供了RAMDirectory可以在內存中容納索引:
Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);
Directory ramDir = new RAMDirectory(fsDir);
Searcher searcher = new IndexSearcher(ramDir);
但是實踐證明RAMDirectory和FSDirectory速度差不多,當數據量很小時兩者都非常快,當數據量較大時(索引文件400M)RAMDirectory甚至比FSDirectory還要慢一點,這確實讓人出乎意料。
而且lucene的搜索非常耗內存,即使將400M的索引文件載入內存,在運行一段時間后都會out of memory,所以個人認為載入內存的作用并不大。
9.2.2 優化時間范圍限制
既然載入內存并不能提高效率,一定有其它瓶頸,經過測試發現最大的瓶頸居然是時間范圍限制,那么我們可以怎樣使時間范圍限制的代價最小呢?
當需要搜索指定時間范圍內的結果時,可以:
1、用RangeQuery,設置范圍,但是RangeQuery的實現實際上是將時間范圍內的時間點展開,組成一個個BooleanClause加入到
BooleanQuery中查詢,
因此時間范圍不可能設置太大,經測試,范圍超過一個月就會拋BooleanQuery.TooManyClauses,可以通過設置
BooleanQuery.setMaxClauseCount(int
maxClauseCount)擴大,但是擴大也是有限的,并且隨著maxClauseCount擴大,占用內存也擴大
2、用RangeFilter代替RangeQuery,經測試速度不會比RangeQuery慢,但是仍然有性能瓶頸,查詢的90%以上時間耗費在
RangeFilter,研究其源碼發現RangeFilter實際上是首先遍歷所有索引,生成一個BitSet,標記每個document,在時間范圍
內的標記為true,不在的標記為false,然后將結果傳遞給Searcher查找,這是十分耗時的。
3、進一步提高性能,這個又有兩個思路:
a、緩存Filter結果。既然RangeFilter的執行是在搜索之前,那么它的輸入都是一定的,就是IndexReader,而
IndexReader是由Directory決定的,所以可以認為RangeFilter的結果是由范圍的上下限決定的,也就是由具體的
RangeFilter對象決定,所以我們只要以RangeFilter對象為鍵,將filter結果BitSet緩存起來即可。lucene
API已經提供了一個CachingWrapperFilter類封裝了Filter及其結果,所以具體實施起來我們可以cache
CachingWrapperFilter對象,需要注意的是,不要被CachingWrapperFilter的名字及其說明誤
導,CachingWrapperFilter看起來是有緩存功能,但的緩存是針對同一個filter的,也就是在你用同一個filter過濾不同
IndexReader時,它可以幫你緩存不同IndexReader的結果,而我們的需求恰恰相反,我們是用不同filter過濾同一個
IndexReader,所以只能把它作為一個封裝類。
b、降低時間精度。研究Filter的工作原理可以看出,它每次工作都是遍歷整個索引的,所以時間粒度越大,對比越快,搜索時間越短,在不影響功能的情況下,時間精度越低越好,有時甚至犧牲一點精度也值得,當然最好的情況是根本不作時間限制。
下面針對上面的兩個思路演示一下優化結果(都采用800線程隨機關鍵詞隨即時間范圍):
第一組,時間精度為秒:
方式 直接用RangeFilter 使用cache 不用filter
平均每個線程耗時 10s 1s 300ms
第二組,時間精度為天
方式 直接用RangeFilter 使用cache 不用filter
平均每個線程耗時 900ms 360ms 300ms
由以上數據可以得出結論:
1、 盡量降低時間精度,將精度由秒換成天帶來的性能提高甚至比使用cache還好,最好不使用filter。
2、 在不能降低時間精度的情況下,使用cache能帶了10倍左右的性能提高。
9.2.3 使用更好的分析器
這個跟創建索引優化道理差不多,索引文件小了搜索自然會加快。當然這個提高也是有限的。較好的分析器相對于最差的分析器對性能的提升在20%以下。
10 一些經驗
10.1關鍵詞區分大小寫
or AND TO等關鍵詞是區分大小寫的,lucene只認大寫的,小寫的當做普通單詞。
10.2 讀寫互斥性
同一時刻只能有一個對索引的寫操作,在寫的同時可以進行搜索
10.3 文件鎖
在寫索引的過程中強行退出將在tmp目錄留下一個lock文件,使以后的寫操作無法進行,可以將其手工刪除
10.4 時間格式
lucene只支持一種時間格式yyMMddHHmmss,所以你傳一個yy-MM-dd HH:mm:ss的時間給lucene它是不會當作時間來處理的
10.5 設置boost
有些時候在搜索時某個字段的權重需要大一些,例如你可能認為標題中出現關鍵詞的文章比正文中出現關鍵詞的文章更有價值,你可以把標題的boost設置的更大,那么搜索結果會優先顯示標題中出現關鍵詞的文章(沒有使用排序的前題下)。使用方法:
Field. setBoost(float boost);默認值是1.0,也就是說要增加權重的需要設置得比1大。
上面這篇關于性能的講解是很深刻. 請學習.
ExtJS教程-
Hibernate教程-
Struts2 教程-
Lucene教程