1 lucene簡(jiǎn)介
1.1 什么是lucene
Lucene是一個(gè)全文搜索框架,而不是應用產(chǎn)品。因此它并不像www.baidu.com 或者google Desktop那么拿來(lái)就能用,它只是提供了一種工具讓你能實(shí)現這些產(chǎn)品。
1.2 lucene能做什么
要回答這個(gè)問(wèn)題,先要了解lucene的本質(zhì)。實(shí)際上lucene的功能很單一,說(shuō)到底,就是你給它若干個(gè)字符串,然后它為你提供一個(gè)全文搜索服務(wù),告訴你你要搜索的關(guān)鍵詞出現在哪里。知道了這個(gè)本質(zhì),你就可以發(fā)揮想象做任何符合這個(gè)條件的事情了。你可以把站內新聞都索引了,做個(gè)資料庫;你可以把一個(gè)數據庫表的若干個(gè)字段索引起來(lái),那就不用再擔心因為“%like%”而鎖表了;你也可以寫(xiě)個(gè)自己的搜索引擎……
1.3 你該不該選擇lucene
下面給出一些測試數據,如果你覺(jué)得可以接受,那么可以選擇。
測試一:250萬(wàn)記錄,300M左右文本,生成索引380M左右,800線(xiàn)程下平均處理時(shí)間300ms。
測試二:37000記錄,索引數據庫中的兩個(gè)varchar字段,索引文件2.6M,800線(xiàn)程下平均處理時(shí)間1.5ms。
2 lucene的工作方式
lucene提供的服務(wù)實(shí)際包含兩部分:一入一出。所謂入是寫(xiě)入,即將你提供的源(本質(zhì)是字符串)寫(xiě)入索引或者將其從索引中刪除;所謂出是讀出,即向用戶(hù)提供全文搜索服務(wù),讓用戶(hù)可以通過(guò)關(guān)鍵詞定位源。
2.1寫(xiě)入流程
源字符串首先經(jīng)過(guò)analyzer處理,包括:分詞,分成一個(gè)個(gè)單詞;去除stopword(可選)。
將源中需要的信息加入Document的各個(gè)Field中,并把需要索引的Field索引起來(lái),把需要存儲的Field存儲起來(lái)。
將索引寫(xiě)入存儲器,存儲器可以是內存或磁盤(pán)。
2.2讀出流程
用戶(hù)提供搜索關(guān)鍵詞,經(jīng)過(guò)analyzer處理。
對處理后的關(guān)鍵詞搜索索引找出對應的Document。
用戶(hù)根據需要從找到的Document中提取需要的Field。
3 一些需要知道的概念
lucene用到一些概念,了解它們的含義,有利于下面的講解。
3.1 analyzer
Analyzer是分析器,它的作用是把一個(gè)字符串按某種規則劃分成一個(gè)個(gè)詞語(yǔ),并去除其中的無(wú)效詞語(yǔ),這里說(shuō)的無(wú)效詞語(yǔ)是指英文中的 “of”、 “the”,中文中的“的”、“地”等詞語(yǔ),這些詞語(yǔ)在文章中大量出現,但是本身不包含什么關(guān)鍵信息,去掉有利于縮小索引文件、提高效率、提高命中率。
分詞的規則千變萬(wàn)化,但目的只有一個(gè):按語(yǔ)義劃分。這點(diǎn)在英文中比較容易實(shí)現,因為英文本身就是以單詞為單位的,已經(jīng)用空格分開(kāi);而中文則必須以某種方法將連成一片的句子劃分成一個(gè)個(gè)詞語(yǔ)。具體劃分方法下面再詳細介紹,這里只需了解分析器的概念即可。
3.2 document
用戶(hù)提供的源是一條條記錄,它們可以是文本文件、字符串或者數據庫表的一條記錄等等。一條記錄經(jīng)過(guò)索引之后,就是以一個(gè)Document的形式存儲在索引文件中的。用戶(hù)進(jìn)行搜索,也是以Document列表的形式返回。
3.3 field
一個(gè)Document可以包含多個(gè)信息域,例如一篇文章可以包含“標題”、“正文”、“最后修改時(shí)間”等信息域,這些信息域就是通過(guò)Field在Document中存儲的。
Field有兩個(gè)屬性可選:存儲和索引。通過(guò)存儲屬性你可以控制是否對這個(gè)Field進(jìn)行存儲;通過(guò)索引屬性你可以控制是否對該Field進(jìn)行索引。這看起來(lái)似乎有些廢話(huà),事實(shí)上對這兩個(gè)屬性的正確組合很重要,下面舉例說(shuō)明:
還是以剛才的文章為例子,我們需要對標題和正文進(jìn)行全文搜索,所以我們要把索引屬性設置為真,同時(shí)我們希望能直接從搜索結果中提取文章標題,所以我們把標題域的存儲屬性設置為真,但是由于正文域太大了,我們?yōu)榱丝s小索引文件大小,將正文域的存儲屬性設置為假,當需要時(shí)再直接讀取文件;我們只是希望能從搜索解果中提取最后修改時(shí)間,不需要對它進(jìn)行搜索,所以我們把最后修改時(shí)間域的存儲屬性設置為真,索引屬性設置為假。上面的三個(gè)域涵蓋了兩個(gè)屬性的三種組合,還有一種全為假的沒(méi)有用到,事實(shí)上Field不允許你那么設置,因為既不存儲又不索引的域是沒(méi)有意義的。
3.4 term
term是搜索的最小單位,它表示文檔的一個(gè)詞語(yǔ),term由兩部分組成:它表示的詞語(yǔ)和這個(gè)詞語(yǔ)所出現的field。
3.5 tocken
tocken是term的一次出現,它包含trem文本和相應的起止偏移,以及一個(gè)類(lèi)型字符串。一句話(huà)中可以出現多次相同的詞語(yǔ),它們都用同一個(gè)term表示,但是用不同的tocken,每個(gè)tocken標記該詞語(yǔ)出現的地方。
3.6 segment
添加索引時(shí)并不是每個(gè)document都馬上添加到同一個(gè)索引文件,它們首先被寫(xiě)入到不同的小文件,然后再合并成一個(gè)大索引文件,這里每個(gè)小文件都是一個(gè)segment。
4 lucene的結構
lucene包括core和sandbox兩部分,其中core是lucene穩定的核心部分,sandbox包含了一些附加功能,例如highlighter、各種分析器。
Lucene core有七個(gè)包:analysis,document,index,queryParser,search,store,util。
4.1 analysis
Analysis包含一些內建的分析器,例如按空白字符分詞的WhitespaceAnalyzer,添加了stopwrod過(guò)濾的StopAnalyzer,最常用的StandardAnalyzer。
4.2 document
Document包含文檔的數據結構,例如Document類(lèi)定義了存儲文檔的數據結構,Field類(lèi)定義了Document的一個(gè)域。
4.3 index
Index包含了索引的讀寫(xiě)類(lèi),例如對索引文件的segment進(jìn)行寫(xiě)、合并、優(yōu)化的IndexWriter類(lèi)和對索引進(jìn)行讀取和刪除操作的 IndexReader類(lèi),這里要注意的是不要被IndexReader這個(gè)名字誤導,以為它是索引文件的讀取類(lèi),實(shí)際上刪除索引也是由它完成, IndexWriter只關(guān)心如何將索引寫(xiě)入一個(gè)個(gè)segment,并將它們合并優(yōu)化;IndexReader則關(guān)注索引文件中各個(gè)文檔的組織形式。
4.4 queryParser
QueryParser包含了解析查詢(xún)語(yǔ)句的類(lèi),lucene的查詢(xún)語(yǔ)句和sql語(yǔ)句有點(diǎn)類(lèi)似,有各種保留字,按照一定的語(yǔ)法可以組成各種查詢(xún)。 Lucene有很多種Query類(lèi),它們都繼承自Query,執行各種特殊的查詢(xún),QueryParser的作用就是解析查詢(xún)語(yǔ)句,按順序調用各種 Query類(lèi)查找出結果。
4.5 search
Search包含了從索引中搜索結果的各種類(lèi),例如剛才說(shuō)的各種Query類(lèi),包括TermQuery、BooleanQuery等就在這個(gè)包里。
4.6 store
Store包含了索引的存儲類(lèi),例如Directory定義了索引文件的存儲結構,FSDirectory為存儲在文件中的索引,RAMDirectory為存儲在內存中的索引,MmapDirectory為使用內存映射的索引。
4.7 util
Util包含一些公共工具類(lèi),例如時(shí)間和字符串之間的轉換工具。
5 如何建索引
5.1 最簡(jiǎn)單的能完成索引的代碼片斷
IndexWriter writer = new IndexWriter(“/data/index/”, 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 works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
下面我們分析一下這段代碼。
首先我們創(chuàng )建了一個(gè)writer,并指定存放索引的目錄為“/data/index”,使用的分析器為StandardAnalyzer,第三個(gè)參數說(shuō)明如果已經(jīng)有索引文件在索引目錄下,我們將覆蓋它們。
然后我們新建一個(gè)document。
我們向document添加一個(gè)field,名字是“title”,內容是“lucene introduction”,對它進(jìn)行存儲并索引。
再添加一個(gè)名字是“content”的field,內容是“lucene works well”,也是存儲并索引。
然后我們將這個(gè)文檔添加到索引中,如果有多個(gè)文檔,可以重復上面的操作,創(chuàng )建document并添加。
添加完所有document,我們對索引進(jìn)行優(yōu)化,優(yōu)化主要是將多個(gè)segment合并到一個(gè),有利于提高索引速度。
隨后將writer關(guān)閉,這點(diǎn)很重要。
對,創(chuàng )建索引就這么簡(jiǎn)單!
當然你可能修改上面的代碼獲得更具個(gè)性化的服務(wù)。
5.2 將索引直接寫(xiě)在內存
你需要首先創(chuàng )建一個(gè)RAMDirectory,并將其傳給writer,代碼如下:
Directory dir = new RAMDirectory();
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 works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
5.3 索引文本文件
如果你想把純文本文件索引起來(lái),而不想自己將它們讀入字符串創(chuàng )建field,你可以用下面的代碼創(chuàng )建field:
Field field = new Field("content", new FileReader(file));
這里的file就是該文本文件。該構造函數實(shí)際上是讀去文件內容,并對其進(jìn)行索引,但不存儲。
6 如何維護索引
索引的維護操作都是由IndexReader類(lèi)提供。
6.1 如何刪除索引
lucene提供了兩種從索引中刪除document的方法,一種是
void deleteDocument(int docNum)
這種方法是根據document在索引中的編號來(lái)刪除,每個(gè)document加進(jìn)索引后都會(huì )有個(gè)唯一編號,所以根據編號刪除是一種精確刪除,但是這個(gè)編號是索引的內部結構,一般我們不會(huì )知道某個(gè)文件的編號到底是幾,所以用處不大。另一種是
void deleteDocuments(Term term)
這種方法實(shí)際上是首先根據參數term執行一個(gè)搜索操作,然后把搜索到的結果批量刪除了。我們可以通過(guò)這個(gè)方法提供一個(gè)嚴格的查詢(xún)條件,達到刪除指定document的目的。
下面給出一個(gè)例子:
Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(field, key);
reader.deleteDocuments(term);
reader.close();
6.2 如何更新索引
lucene并沒(méi)有提供專(zhuān)門(mén)的索引更新方法,我們需要先將相應的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();
7 如何搜索
lucene的搜索相當強大,它提供了很多輔助查詢(xún)類(lèi),每個(gè)類(lèi)都繼承自Query類(lèi),各自完成一種特殊的查詢(xún),你可以像搭積木一樣將它們任意組合使用,完成一些復雜操作;另外lucene還提供了Sort類(lèi)對結果進(jìn)行排序,提供了Filter類(lèi)對查詢(xún)條件進(jìn)行限制。你或許會(huì )不自覺(jué)地拿它跟SQL語(yǔ)句進(jìn)行比較:“lucene能執行and、or、order by、where、like ‘%xx%’操作嗎?”回答是:“當然沒(méi)問(wèn)題!”
7.1 各種各樣的Query
下面我們看看lucene到底允許我們進(jìn)行哪些查詢(xún)操作:
7.1.1 TermQuery
首先介紹最基本的查詢(xún),如果你想執行一個(gè)這樣的查詢(xún):“在content域中包含‘lucene’的document”,那么你可以用TermQuery:
Term t = new Term("content", " lucene";
Query query = new TermQuery(t);
7.1.2 BooleanQuery
如果你想這么查詢(xún):“在content域中包含java或perl的document”,那么你可以建立兩個(gè)TermQuery并把它們用BooleanQuery連接起來(lái):
TermQuery termQuery1 = new TermQuery(new Term("content", "java");
TermQuery termQuery 2 = new TermQuery(new Term("content", "perl");
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);
booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);
7.1.3 WildcardQuery
如果你想對某單詞進(jìn)行通配符查詢(xún),你可以用WildcardQuery,通配符包括’?’匹配一個(gè)任意字符和’*’匹配零個(gè)或多個(gè)任意字符,例如你搜索’use*’,你可能找到’useful’或者’useless’:
Query query = new WildcardQuery(new Term("content", "use*");
7.1.4 PhraseQuery
你可能對中日關(guān)系比較感興趣,想查找‘中’和‘日’挨得比較近(5個(gè)字的距離內)的文章,超過(guò)這個(gè)距離的不予考慮,你可以:
PhraseQuery query = new PhraseQuery();
query.setSlop(5);
query.add(new Term("content ", “中”));
query.add(new Term(“content”, “日”));
那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中國某高層領(lǐng)導說(shuō)日本欠扁”。
7.1.5 PrefixQuery
如果你想搜以‘中’開(kāi)頭的詞語(yǔ),你可以用PrefixQuery:
PrefixQuery query = new PrefixQuery(new Term("content ", "中");
7.1.6 FuzzyQuery
FuzzyQuery用來(lái)搜索相似的term,使用Levenshtein算法。假設你想搜索跟‘wuzza’相似的詞語(yǔ),你可以:
Query query = new FuzzyQuery(new Term("content", "wuzza");
你可能得到‘fuzzy’和‘wuzzy’。
7.1.7 RangeQuery
另一個(gè)常用的Query是RangeQuery,你也許想搜索時(shí)間域從20060101到20060130之間的document,你可以用RangeQuery:
RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true);
最后的true表示用閉合區間。
7.2 QueryParser
看了這么多Query,你可能會(huì )問(wèn):“不會(huì )讓我自己組合各種Query吧,太麻煩了!”當然不會(huì ),lucene提供了一種類(lèi)似于SQL語(yǔ)句的查詢(xún)語(yǔ)句,我們姑且叫它lucene語(yǔ)句,通過(guò)它,你可以把各種查詢(xún)一句話(huà)搞定,lucene會(huì )自動(dòng)把它們查分成小塊交給相應Query執行。下面我們對應每種 Query演示一下:
TermQuery可以用“field:key”方式,例如“content:lucene”。
BooleanQuery中‘與’用‘+’,‘或’用‘ ’,例如“content:java contenterl”。
WildcardQuery仍然用‘?’和‘*’,例如“content:use*”。
PhraseQuery用‘~’,例如“content:"中日"~5”。
PrefixQuery用‘*’,例如“中*”。
FuzzyQuery用‘~’,例如“content: wuzza ~”。
RangeQuery用‘[]’或‘{}’,前者表示閉區間,后者表示開(kāi)區間,例如“time:[20060101 TO 20060130]”,注意TO區分大小寫(xiě)。
你可以任意組合query string,完成復雜操作,例如“標題或正文包括lucene,并且時(shí)間在20060101到20060130之間的文章” 可以表示為:“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代碼如下:
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) +time:[20060101 TO 20060130]";
Hits hits = is.search(query);
for (int i = 0; i < hits.length(); i++)
{
Document doc = hits.doc(i);
System.out.println(doc.get("title");
}
is.close();
首先我們創(chuàng )建一個(gè)在指定文件目錄上的IndexSearcher。
然后創(chuàng )建一個(gè)使用StandardAnalyzer作為分析器的QueryParser,它默認搜索的域是content。
接著(zhù)我們用QueryParser來(lái)parse查詢(xún)字串,生成一個(gè)Query。
然后利用這個(gè)Query去查找結果,結果以Hits的形式返回。
這個(gè)Hits對象包含一個(gè)列表,我們挨個(gè)把它的內容顯示出來(lái)。
7.3 Filter
filter的作用就是限制只查詢(xún)索引的某個(gè)子集,它的作用有點(diǎn)像SQL語(yǔ)句里的 where,但又有區別,它不是正規查詢(xún)的一部分,只是對數據源進(jìn)行預處理,然后交給查詢(xún)語(yǔ)句。注意它執行的是預處理,而不是對查詢(xún)結果進(jìn)行過(guò)濾,所以使用filter的代價(jià)是很大的,它可能會(huì )使一次查詢(xún)耗時(shí)提高一百倍。
最常用的filter是RangeFilter和QueryFilter。RangeFilter是設定只搜索指定范圍內的索引;QueryFilter是在上次查詢(xún)的結果中搜索。
Filter的使用非常簡(jiǎn)單,你只需創(chuàng )建一個(gè)filter實(shí)例,然后把它傳給searcher。繼續上面的例子,查詢(xún)“時(shí)間在20060101到20060130之間的文章”除了將限制寫(xiě)在query string中,你還可以寫(xiě)在RangeFilter中:
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);
Hits hits = is.search(query, filter);
for (int i i < hits.length(); i++)
{
Document doc = hits.doc(i);
System.out.println(doc.get("title");
}
is.close();
7.4 Sort
有時(shí)你想要一個(gè)排好序的結果集,就像SQL語(yǔ)句的“order by”,lucene能做到:通過(guò)Sort。
Sort sort Sort(“time”); //相當于SQL的“order by time”
Sort sort = new Sort(“time”, true); // 相當于SQL的“order by time desc”
下面是一個(gè)完整的例子:
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();
8 分析器
在前面的概念介紹中我們已經(jīng)知道了分析器的作用,就是把句子按照語(yǔ)義切分成一個(gè)個(gè)詞語(yǔ)。英文切分已經(jīng)有了很成熟的分析器: StandardAnalyzer,很多情況下StandardAnalyzer是個(gè)不錯的選擇。甚至你會(huì )發(fā)現StandardAnalyzer也能對中文進(jìn)行分詞。
但是我們的焦點(diǎn)是中文分詞,StandardAnalyzer能支持中文分詞嗎?實(shí)踐證明是可以的,但是效果并不好,搜索“如果”會(huì )把“牛奶不如果汁好喝 ”也搜索出來(lái),而且索引文件很大。那么我們手頭上還有什么分析器可以使用呢?core里面沒(méi)有,我們可以在sandbox里面找到兩個(gè): ChineseAnalyzer和CJKAnalyzer。但是它們同樣都有分詞不準的問(wèn)題。相比之下用StandardAnalyzer 和 ChineseAnalyzer建立索引時(shí)間差不多,索引文件大小也差不多,CJKAnalyzer表現會(huì )差些,索引文件大且耗時(shí)比較長(cháng)。
要解決問(wèn)題,首先分析一下這三個(gè)分析器的分詞方式。StandardAnalyzer和ChineseAnalyzer都是把句子按單個(gè)字切分,也就是說(shuō) “牛奶不如果汁好喝”會(huì )被它們切分成“牛 奶 不 如 果 汁 好 喝”;而CJKAnalyzer則會(huì )切分成“牛奶 奶不 不如 如果 果汁 汁好好喝”。這也就解釋了為什么搜索“果汁”都能匹配這個(gè)句子。
以上分詞的缺點(diǎn)至少有兩個(gè):匹配不準確和索引文件大。我們的目標是將上面的句子分解成“牛奶 不如 果汁好喝”。這里的關(guān)鍵就是語(yǔ)義識別,我們如何識別“ 牛奶”是一個(gè)詞而“奶不”不是詞語(yǔ)?我們很自然會(huì )想到基于詞庫的分詞法,也就是我們先得到一個(gè)詞庫,里面列舉了大部分詞語(yǔ),我們把句子按某種方式切分,當得到的詞語(yǔ)與詞庫中的項匹配時(shí),我們就認為這種切分是正確的。這樣切詞的過(guò)程就轉變成匹配的過(guò)程,而匹配的方式最簡(jiǎn)單的有正向最大匹配和逆向最大匹配兩種,說(shuō)白了就是一個(gè)從句子開(kāi)頭向后進(jìn)行匹配,一個(gè)從句子末尾向前進(jìn)行匹配?;谠~庫的分詞詞庫非常重要,詞庫的容量直接影響搜索結果,在相同詞庫的前提下,據說(shuō)逆向最大匹配優(yōu)于正向最大匹配。
當然還有別的分詞方法,這本身就是一個(gè)學(xué)科,我這里也沒(méi)有深入研究?;氐骄唧w應用,我們的目標是能找到成熟的、現成的分詞工具,避免重新發(fā)明車(chē)輪。經(jīng)過(guò)網(wǎng)上搜索,用的比較多的是中科院的ICTCLAS和一個(gè)不開(kāi)放源碼但是免費的JE-Analysis。ICTCLAS有個(gè)問(wèn)題是它是一個(gè)動(dòng)態(tài)鏈接庫, java調用需要本地方法調用,不方便也有安全隱患,而且口碑也確實(shí)不大好。JE-Analysis效果還不錯,當然也會(huì )有分詞不準的地方,相比比較方便放心。= new = 0;
9 性能優(yōu)化
一直到這里,我們還是在討論怎么樣使lucene跑起來(lái),完成指定任務(wù)。利用前面說(shuō)的也確實(shí)能完成大部分功能。但是測試表明lucene的性能并不是很好,在大數據量大并發(fā)的條件下甚至會(huì )有半分鐘返回的情況。另外大數據量的數據初始化建立索引也是一個(gè)十分耗時(shí)的過(guò)程。那么如何提高lucene的性能呢?下面從優(yōu)化創(chuàng )建索引性能和優(yōu)化搜索性能兩方面介紹。
9.1 優(yōu)化創(chuàng )建索引性能
這方面的優(yōu)化途徑比較有限,IndexWriter提供了一些接口可以控制建立索引的操作,另外我們可以先將索引寫(xiě)入RAMDirectory,再批量寫(xiě)入FSDirectory,不管怎樣,目的都是盡量少的文件IO,因為創(chuàng )建索引的最大瓶頸在于磁盤(pán)IO。另外選擇一個(gè)較好的分析器也能提高一些性能。
9.1.1 通過(guò)設置IndexWriter的參數優(yōu)化索引建立
setMaxBufferedDocs(int maxBufferedDocs)
控制寫(xiě)入一個(gè)新的segment前內存中保存的document的數目,設置較大的數目可以加快建索引速度,默認為10。
setMaxMergeDocs(int maxMergeDocs)
控制一個(gè)segment中可以保存的最大document數目,值較小有利于追加索引的速度,默認Integer.MAX_VALUE,無(wú)需修改。
setMergeFactor(int mergeFactor)
控制多個(gè)segment合并的頻率,值較大時(shí)建立索引速度較快,默認是10,可以在建立索引時(shí)設置為100。
9.1.2 通過(guò)RAMDirectory緩寫(xiě)提高性能
我們可以先把索引寫(xiě)入RAMDirectory,達到一定數量時(shí)再批量寫(xiě)進(jìn)FSDirectory,減少磁盤(pán)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 選擇較好的分析器
這個(gè)優(yōu)化主要是對磁盤(pán)空間的優(yōu)化,可以將索引文件減小將近一半,相同測試數據下由600M減少到380M。但是對時(shí)間并沒(méi)有什么幫助,甚至會(huì )需要更長(cháng)時(shí)間,因為較好的分析器需要匹配詞庫,會(huì )消耗更多cpu,測試數據用StandardAnalyzer耗時(shí)133分鐘;用MMAnalyzer耗時(shí)150分鐘。
9.2 優(yōu)化搜索性能
雖然建立索引的操作非常耗時(shí),但是那畢竟只在最初創(chuàng )建時(shí)才需要,平時(shí)只是少量的維護操作,更何況這些可以放到一個(gè)后臺進(jìn)程處理,并不影響用戶(hù)搜索。我們創(chuàng )建索引的目的就是給用戶(hù)搜索,所以搜索的性能才是我們最關(guān)心的。下面就來(lái)探討一下如何提高搜索性能。
9.2.1 將索引放入內存
這是一個(gè)最直觀(guān)的想法,因為內存比磁盤(pán)快很多。Lucene提供了RAMDirectory可以在內存中容納索引:
Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);
Directory ramDir = new RAMDirectory(fsDir);
Searcher searcher = new IndexSearcher(ramDir);
但是實(shí)踐證明RAMDirectory和FSDirectory速度差不多,當數據量很小時(shí)兩者都非???,當數據量較大時(shí)(索引文件400M)RAMDirectory甚至比FSDirectory還要慢一點(diǎn),這確實(shí)讓人出乎意料。
而且lucene的搜索非常耗內存,即使將400M的索引文件載入內存,在運行一段時(shí)間后都會(huì )out of memory,所以個(gè)人認為載入內存的作用并不大。
9.2.2 優(yōu)化時(shí)間范圍限制
既然載入內存并不能提高效率,一定有其它瓶頸,經(jīng)過(guò)測試發(fā)現最大的瓶頸居然是時(shí)間范圍限制,那么我們可以怎樣使時(shí)間范圍限制的代價(jià)最小呢?
當需要搜索指定時(shí)間范圍內的結果時(shí),可以:
1、用RangeQuery,設置范圍,但是RangeQuery的實(shí)現實(shí)際上是將時(shí)間范圍內的時(shí)間點(diǎn)展開(kāi),組成一個(gè)個(gè)BooleanClause加入到 BooleanQuery中查詢(xún),因此時(shí)間范圍不可能設置太大,經(jīng)測試,范圍超過(guò)一個(gè)月就會(huì )拋 BooleanQuery.TooManyClauses,可以通過(guò)設置 BooleanQuery.setMaxClauseCount(int maxClauseCount)擴大,但是擴大也是有限的,并且隨著(zhù) maxClauseCount擴大,占用內存也擴大
2、用RangeFilter代替RangeQuery,經(jīng)測試速度不會(huì )比RangeQuery慢,但是仍然有性能瓶頸,查詢(xún)的90%以上時(shí)間耗費在 RangeFilter,研究其源碼發(fā)現RangeFilter實(shí)際上是首先遍歷所有索引,生成一個(gè)BitSet,標記每個(gè)document,在時(shí)間范圍內的標記為true,不在的標記為false,然后將結果傳遞給Searcher查找,這是十分耗時(shí)的。
3、進(jìn)一步提高性能,這個(gè)又有兩個(gè)思路:
a、緩存Filter結果。既然RangeFilter的執行是在搜索之前,那么它的輸入都是一定的,就是IndexReader,而 IndexReader是由Directory決定的,所以可以認為RangeFilter的結果是由范圍的上下限決定的,也就是由具體的 RangeFilter對象決定,所以我們只要以RangeFilter對象為鍵,將filter結果BitSet緩存起來(lái)即可。 lucene API已經(jīng)提供了一個(gè)CachingWrapperFilter類(lèi)封裝了Filter及其結果,所以具體實(shí)施起來(lái)我們可以 cache CachingWrapperFilter對象,需要注意的是,不要被CachingWrapperFilter的名字及其說(shuō)明誤導, CachingWrapperFilter看起來(lái)是有緩存功能,但的緩存是針對同一個(gè)filter的,也就是在你用同一個(gè)filter過(guò)濾不同 IndexReader時(shí),它可以幫你緩存不同IndexReader的結果,而我們的需求恰恰相反,我們是用不同filter過(guò)濾同一個(gè) IndexReader,所以只能把它作為一個(gè)封裝類(lèi)。
b、降低時(shí)間精度。研究Filter的工作原理可以看出,它每次工作都是遍歷整個(gè)索引的,所以時(shí)間粒度越大,對比越快,搜索時(shí)間越短,在不影響功能的情況下,時(shí)間精度越低越好,有時(shí)甚至犧牲一點(diǎn)精度也值得,當然最好的情況是根本不作時(shí)間限制。
下面針對上面的兩個(gè)思路演示一下優(yōu)化結果(都采用800線(xiàn)程隨機關(guān)鍵詞隨即時(shí)間范圍):
第一組,時(shí)間精度為秒:
方式 直接用RangeFilter 使用cache 不用filter
平均每個(gè)線(xiàn)程耗時(shí) 10s 1s 300ms
第二組,時(shí)間精度為天
方式 直接用RangeFilter 使用cache 不用filter
平均每個(gè)線(xiàn)程耗時(shí) 900ms 360ms 300ms
由以上數據可以得出結論:
1、 盡量降低時(shí)間精度,將精度由秒換成天帶來(lái)的性能提高甚至比使用cache還好,最好不使用filter。
2、 在不能降低時(shí)間精度的情況下,使用cache能帶了10倍左右的性能提高。
9.2.3 使用更好的分析器
這個(gè)跟創(chuàng )建索引優(yōu)化道理差不多,索引文件小了搜索自然會(huì )加快。當然這個(gè)提高也是有限的。較好的分析器相對于最差的分析器對性能的提升在20%以下。
10 一些經(jīng)驗
10.1關(guān)鍵詞區分大小寫(xiě)
or AND TO等關(guān)鍵詞是區分大小寫(xiě)的,lucene只認大寫(xiě)的,小寫(xiě)的當做普通單詞。
10.2 讀寫(xiě)互斥性
同一時(shí)刻只能有一個(gè)對索引的寫(xiě)操作,在寫(xiě)的同時(shí)可以進(jìn)行搜索
10.3 文件鎖
在寫(xiě)索引的過(guò)程中強行退出將在tmp目錄留下一個(gè)lock文件,使以后的寫(xiě)操作無(wú)法進(jìn)行,可以將其手工刪除
10.4 時(shí)間格式
lucene只支持一種時(shí)間格式yyMMddHHmmss,所以你傳一個(gè)yy-MM-dd HH:mm:ss的時(shí)間給lucene它是不會(huì )當作時(shí)間來(lái)處理的
10.5 設置boost
有些時(shí)候在搜索時(shí)某個(gè)字段的權重需要大一些,例如你可能認為標題中出現關(guān)鍵詞的文章比正文中出現關(guān)鍵詞的文章更有價(jià)值,你可以把標題的boost設置的更大,那么搜索結果會(huì )優(yōu)先顯示標題中出現關(guān)鍵詞的文章(沒(méi)有使用排序的前題下)。使用方法:
Field. setBoost(float boost);默認值是1.0,也就是說(shuō)要增加權重的需要設置得比1大。