





介紹開(kāi)源軟件搜索引擎——lucene的各個(gè)實(shí)現的功能,性能,以及代
| 分析的系統名稱(chēng) | Lucene |
| 該開(kāi)源主頁(yè) | |
| 開(kāi)發(fā)語(yǔ)言 | JAVA |
| 該系統的分析者 | zzpchina |
| 該系統作者簡(jiǎn)介 | Lucene的貢獻者Doug Cutting是一位資深全文索引/檢索專(zhuān)家,曾經(jīng)是V-Twin搜索引擎(Apple的Copland操作系統的成就之一)的主要開(kāi)發(fā)者,后在Excite擔任高級系統架構設計師,目前從 |
| 該系統簡(jiǎn)介 | Lucene不是一個(gè)完整的全文索引應用,而 |
經(jīng)過(guò)多年的發(fā)展,Lucene在全文檢索領(lǐng)域已經(jīng)有了很多的成
基于Lucene的全文檢索產(chǎn)品和應用Lucene的項目在世界各地已經(jīng)非常之多,
<!--[if !supportLists]-->1. <!--[endif]--> Eclipse:主流Java開(kāi)發(fā)工具,其幫助文檔采用Lucene作為檢索引擎
<!--[if !supportLists]-->2. <!--[endif]-->Jive:知名論壇系統,其檢索功能基于
<!--[if !supportLists]-->3. <!--[endif]-->Ifinder:出自德國的網(wǎng)站檢索系統,基于
<!--[if !supportLists]-->4. <!--[endif]-->MIT DSpace Federation:一個(gè)文檔管理系統(http://www.dspa
國內外采用Lucene作為網(wǎng)站全文檢索引擎的也很多,
<!--[if !supportLists]-->1. <!--[endif]-->http://www.blog
<!--[if !supportLists]-->2. <!--[endif]-->http://www.ioff
<!--[if !supportLists]-->3. <!--[endif]-->http://search.s
<!--[if !supportLists]-->4. <!--[endif]-->http://www.tami
(更多案例,參見(jiàn)http://wiki.apa
在所有這些案例中,開(kāi)源應用占了
Lucene的API接口設計的比較通用,輸入輸出結構都很像數據庫的表==>記錄==>字段,所以很多傳統的應用的文件、數據庫等都可以比較方便的映射到Lucene的存儲結構/接口中??傮w上看:可以先把Lucene當成一個(gè)支持全文索引的數據庫系統。
| 全文檢索庫對關(guān)系型數據庫對比 | ||
| 對比項 | 全文檢索庫(Lucene) | 關(guān)系型數據庫(Oracle) |
| 核心功能 | 以文本檢索為主,插入(insert)、刪除(delete)、修改(update)比較麻煩,適合于大文本塊的查詢(xún)。 | 插入(insert)、刪除(delete)、修改(update)十分方便,有專(zhuān)門(mén)的SQL命令,但對于大文本塊(如CLOB)類(lèi)型的檢索效率低下。 |
| 庫 | 與Oracle類(lèi)似,都可以建多個(gè)庫,且各個(gè)庫的存儲位置可以不同。 | 可以建多個(gè)庫,每個(gè)庫一般都有控制文件和數據文件等,比較復雜。 |
| 表 | 沒(méi)有嚴格的表的概念,比如Lucene的表只是由入庫時(shí)的定義字段松散組成。 | 有嚴格的表結構,有主鍵,有字段類(lèi)型等。 |
| 記錄 | 由于沒(méi)有嚴格表的概念,所以記錄體現為一個(gè)對象,在Lucene里記錄對應的類(lèi)是Document。 | Record,與表結構對應。 |
| 字段 | 字段類(lèi)型只有文本和日期兩種,字段一般不支持運算,更無(wú)函數功能。 在Lucene里字段的類(lèi)是Field,如document(field1,field2…) | 字段類(lèi)型豐富,功能強大。 record(field1,field2…) |
| 查詢(xún)結果集 | 在Lucene里表示查詢(xún)結果集的類(lèi)是Hits,如hits(doc1,doc2,doc3…) | 在JDBC為例, Resultset(record1,record2,record3...) |
全文檢索 ≠ like "%keyword%"
通常比較厚的書(shū)籍后面常常附關(guān)鍵詞索引表(比如:北京:12, 34頁(yè),上海:3,77頁(yè)……),它能夠幫助讀者比較快地找到相關(guān)內容的頁(yè)碼。而數據庫索引能夠大大提高查詢(xún)的速度原理也是一樣,想像一下通過(guò)書(shū)后面的索引查找的速度要比一頁(yè)一頁(yè)地翻內容高多少倍……而索引之所以效率高,另外一個(gè)原因是它是排好序的。對于檢索系統來(lái)說(shuō)核心是一個(gè)排序問(wèn)題。
由于數據庫索引不是為全文索引設計的,因此,使用like "%keyword%"時(shí),數據庫索引是不起作用的,在使用like查詢(xún)時(shí),搜索過(guò)程又變成類(lèi)似于一頁(yè)頁(yè)翻書(shū)的遍歷過(guò)程了,所以對于含有模糊查詢(xún)的數據庫服務(wù)來(lái)說(shuō),LIKE對性能的危害是極大的。如果是需要對多個(gè)關(guān)鍵詞進(jìn)行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
所以建立一個(gè)高效檢索系統的關(guān)鍵是建立一個(gè)類(lèi)似于科技索引一樣的反向索引機制,將數據源(比如多篇文章)排序順序存儲的同時(shí),有另外一個(gè)排好序的關(guān)鍵詞列表,用于存儲關(guān)鍵詞==>文章映射關(guān)系,利用這樣的映射關(guān)系索引:
關(guān)鍵詞==>出現關(guān)鍵詞的文章編號、出現次數、起始偏移量、結束偏移量,出現頻率
檢索過(guò)程就是把模糊查詢(xún)變成多個(gè)可以利用索引的精確查詢(xún)的邏輯組合的過(guò)程。從而大大提高了多關(guān)鍵詞查詢(xún)的效率,所以,全文檢索問(wèn)題歸結到最后是一個(gè)排序問(wèn)題。
由此可以看出,模糊查詢(xún)相對數據庫的精確查詢(xún)是一個(gè)非常不確定的問(wèn)題,這也是大部分數據庫對全文檢索支持有限的原因。Lucene最核心的特征是通過(guò)特殊的索引結構實(shí)現了傳統數據庫不擅長(cháng)的全文索引機制,并提供了擴展接口,以方便針對不同應用的定制。
可以通過(guò)一下表格對比一下數據庫的模糊查詢(xún):
| | Lucene全文索引引擎 | 數據庫 |
| 索引 | 將數據源中的數據都通過(guò)全文索引一一建立反向索引 | 對于LIKE查詢(xún)來(lái)說(shuō),數據傳統的索引是根本用不上的。數據需要逐個(gè)便利記錄進(jìn)行GREP式的模糊匹配,比有索引的搜索速度要有多個(gè)數量級的下降。 |
| 匹配效果 | 通過(guò)詞元(term)進(jìn)行匹配,通過(guò)語(yǔ)言分析接口的實(shí)現,可以實(shí)現對中文等非英語(yǔ)的支持。 | 使用:like "%net%" 會(huì )把netherlands也匹配出來(lái), |
| 匹配度 | 有匹配度算法,將匹配程度(相似度)比較高的結果排在前面。 | 沒(méi)有匹配程度的控制:比如有記錄中net出現5詞和出現1次的,結果是一樣的。 |
| 結果輸出 | 通過(guò)特別的算法,將最匹配度最高的頭100條結果輸出,結果集是緩沖式的小批量讀取的。 | 返回所有的結果集,在匹配條目非常多的時(shí)候(比如上萬(wàn)條)需要大量的內存存放這些臨時(shí)結果集。 |
| 可定制性 | 通過(guò)不同的語(yǔ)言分析接口實(shí)現,可以方便的定制出符合應用需要的索引規則(包括對中文的支持) | 沒(méi)有接口或接口復雜,無(wú)法定制 |
| 結論 | 高負載的模糊查詢(xún)應用,需要負責的模糊查詢(xún)的規則,索引的資料量比較大 | 使用率低,模糊匹配規則簡(jiǎn)單或者需要模糊查詢(xún)的資料量少 |
全文檢索和數據庫應用最大的不同在于:讓最相關(guān)的頭100條結果滿(mǎn)足98%以上用戶(hù)的需求
大部分的搜索(數據庫)引擎都是用B樹(shù)結構來(lái)維護索引,索引的更新會(huì )導致大量的IO操作,Lucene在實(shí)現中,對此稍微有所改進(jìn):不是維護一個(gè)索引文件,而是在擴展索引的時(shí)候不斷創(chuàng )建新的索引文件,然后定期的把這些新的小索引文件合并到原先的大索引中(針對不同的更新策略,批次的大小可以調整),這樣在不影響檢索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文檢索系統/應用的比較:
| | Lucene | 其他開(kāi)源全文檢索系統 |
| 增量索引和批量索引 | 可以進(jìn)行增量的索引(Append),可以對于大量數據進(jìn)行批量索引,并且接口設計用于優(yōu)化批量索引和小批量的增量索引。 | 很多系統只支持批量的索引,有時(shí)數據源有一點(diǎn)增加也需要重建索引。 |
| 數據源 | Lucene沒(méi)有定義具體的數據源,而是一個(gè)文檔的結構,因此可以非常靈活的適應各種應用(只要前端有合適的轉換器把數據源轉換成相應結構), | 很多系統只針對網(wǎng)頁(yè),缺乏其他格式文檔的靈活性。 |
| 索引內容抓取 | Lucene的文檔是由多個(gè)字段組成的,甚至可以控制那些字段需要進(jìn)行索引,那些字段不需要索引,近一步索引的字段也分為需要分詞和不需要分詞的類(lèi)型: | 缺乏通用性,往往將文檔整個(gè)索引了 |
| 語(yǔ)言分析 | 通過(guò)語(yǔ)言分析器的不同擴展實(shí)現: | 缺乏通用接口實(shí)現 |
| 查詢(xún)分析 | 通過(guò)查詢(xún)分析接口的實(shí)現,可以定制自己的查詢(xún)語(yǔ)法規則: | |
| 并發(fā)訪(fǎng)問(wèn) | 能夠支持多用戶(hù)的使用 | |
索引一般分2種情況,一種是小批量的索引擴展,一種是大批量的索引重建。在索引過(guò)程中,并不是每次新的DOC加入進(jìn)去索引都重新進(jìn)行一次索引文件的寫(xiě)入操作(文件I/O是一件非常消耗資源的事情)。
Lucene先在內存中進(jìn)行索引操作,并根據一定的批量進(jìn)行文件的寫(xiě)入。這個(gè)批次的間隔越大,文件的寫(xiě)入次數越少,但占用內存會(huì )很多。反之占用內存少,但文件IO操作頻繁,索引速度會(huì )很慢。在IndexWriter中有一個(gè)MERGE_FACTOR參數可以幫助你在構造索引器后根據應用環(huán)境的情況充分利用內存減少文件的操作。根據我的使用經(jīng)驗:缺省Indexer是每20條記錄索引后寫(xiě)入一次,每將MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
lucene支持內存索引:這樣的搜索比基于文件的I/O有數量級的速度提升。
http://www.onjava.com/lpt/a/3273,而盡可能減少IndexSearcher的創(chuàng )建和對搜索結果的前臺的緩存也是必要的。
Lucene面向全文檢索的優(yōu)化在于首次索引檢索后,并不把所有的記錄(Document)具體內容讀取出來(lái),而起只將所有結果中匹配度最高的頭100條結果(TopDocs)的ID放到結果集緩存中并返回,這里可以比較一下數據庫檢索:如果是一個(gè)10,000條的數據庫檢索結果集,數據庫是一定要把所有記錄內容都取得以后再開(kāi)始返回給應用結果集的。
所以即使檢索匹配總數很多,Lucene的結果集占用的內存空間也不會(huì )很多。對于一般的模糊檢索應用是用不到這么多的結果的,頭100條已經(jīng)可以滿(mǎn)足90%以上的檢索需求。
如果首批緩存結果數用完后還要讀取更后面的結果時(shí)Searcher會(huì )再次檢索并生成一個(gè)上次的搜索緩存數大1倍的緩存,并再重新向后抓取。所以如果構造一個(gè)Searcher去查1-120條結果,Searcher其實(shí)是進(jìn)行了2次搜索過(guò)程:頭100條取完后,緩存結果用完,Searcher重新檢索再構造一個(gè)200條的結果緩存,依此類(lèi)推,400條緩存,800條緩存。由于每次Searcher對象消失后,這些緩存也訪(fǎng)問(wèn)那不到了,你有可能想將結果記錄緩存下來(lái),緩存數盡量保證在100以下以充分利用首次的結果緩存,不讓Lucene浪費多次檢索,而且可以分級進(jìn)行結果緩存。
Lucene的另外一個(gè)特點(diǎn)是在收集結果的過(guò)程中將匹配度低的結果自動(dòng)過(guò)濾掉了,過(guò)濾過(guò)程我們可以通過(guò)設置最低的匹配度來(lái)進(jìn)行過(guò)濾。這也是和數據庫應用需要將搜索的結果全部返回不同之處。
Lucene 的索引排序是使用了倒排序原理。
該結構及相應的生成算法如下:
設有兩篇文章1和2
文章1的內容為:Tom lives in
文章2的內容為:He once lived in
<!--[if !supportLists]-->1. <!--[endif]-->由于lucene是基于關(guān)鍵詞索引和查詢(xún)的,首先我們要取得這兩篇文章的關(guān)鍵詞,通常我們需要如下處理措施
<!--[if !supportLists]-->a. <!--[endif]-->我們現在有的是文章內容,即一個(gè)字符串,我們先要找出字符串中的所有單詞,即分詞。英文單詞由于用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
<!--[if !supportLists]-->b. <!--[endif]-->文章中的”in”, “once” “too”等詞沒(méi)有什么實(shí)際意義,中文中的“的”“是”等字通常也無(wú)具體含義, 這些不代表概念的詞可以過(guò)濾掉,這個(gè)也就是在《Lucene詳細分析》中所講的StopTokens
<!--[if !supportLists]-->c. <!--[endif]-->用戶(hù)通常希望查“He”時(shí)能把含“he”,“HE”的文章也找出來(lái),所以所有單詞需要統一大小寫(xiě)。
<!--[if !supportLists]-->d. <!--[endif]-->用戶(hù)通常希望查“live”時(shí)能把含“lives”,“lived”的文章也找出來(lái),所以需要把“lives”,“lived”還原成“live”
<!--[if !supportLists]-->e. <!--[endif]-->文章中的標點(diǎn)符號通常不表示某種概念,也可以過(guò)濾掉,在lucene中以上措施由Analyzer類(lèi)完成,經(jīng)過(guò)上面處理后:
文章1的所有關(guān)鍵詞為:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有關(guān)鍵詞為:[he] [live] [shanghai]
<!--[if !supportLists]-->2. <!--[endif]-->有了關(guān)鍵詞后,我們就可以建立倒排索引了
上面的對應關(guān)系是:“文章號”對“文章中所有關(guān)鍵詞”。倒排索引把這個(gè)關(guān)系倒過(guò)來(lái),變成:“關(guān)鍵詞”對“擁有該關(guān)鍵詞的所有文章號”。文章1,2經(jīng)過(guò)倒排后變成
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->
| 關(guān)鍵詞 | 文章號 |
| | 1 |
| he | 2 |
| i | 1 |
| live | 1,2 |
| shanghai | 2 |
| tom | 1 |
通常僅知道關(guān)鍵詞在哪些文章中出現還不夠,我們還需要知道關(guān)鍵詞在文章中出現次數和出現的位置,通常有兩種位置:a)字符位置,即記錄該詞是文章中第幾個(gè)字符(優(yōu)點(diǎn)是關(guān)鍵詞亮顯時(shí)定位快);b)關(guān)鍵詞位置,即記錄該詞是文章中第幾個(gè)關(guān)鍵詞(優(yōu)點(diǎn)是節約索引空間、詞組(phase)查詢(xún)快),lucene中記錄的就是這種位置。
加上“出現頻率”和“出現位置”信息后,我們的索引結構變?yōu)椋?/span>
| 關(guān)鍵詞 | 文章號[出現頻率] | 出現位置 |
| | 1[2] | 3,6 |
| he | 2[1] | 1 |
| i | 1[1] | 4 |
| live | 1[2],2[1] | 2,5,2 |
| shanghai | 2[1] | 3 |
| tom | 1[1] | 1 |
以live 這行為例我們說(shuō)明一下該結構:live在文章1中出現了2次,文章2中出現了一次,它的出現位置為“2,5,
以上就是lucene索引結構中最核心的部分。我們注意到關(guān)鍵字是按字符順序排列的(lucene沒(méi)有使用B樹(shù)結構),因此lucene可以用二元搜索算法快速定位關(guān)鍵詞。
實(shí)現時(shí) lucene將上面三列分別作為詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。其中詞典文件不僅保存有每個(gè)關(guān)鍵詞,還保留了指向頻率文件和位置文件的指針,通過(guò)指針可以找到該關(guān)鍵字的頻率信息和位置信息。
Lucene中使用了field的概念,用于表達信息所在位置(如標題中,文章中,url中),在建索引中,該field信息也記錄在詞典文件中,每個(gè)關(guān)鍵詞都有一個(gè)field信息(因為每個(gè)關(guān)鍵字一定屬于一個(gè)或多個(gè)field)。
為了減小索引文件的大小,Lucene對索引還使用了壓縮技術(shù)。首先,對詞典文件中的關(guān)鍵詞進(jìn)行了壓縮,關(guān)鍵詞壓縮為<前綴長(cháng)度,后綴>,例如:當前詞為“阿拉伯語(yǔ)”,上一個(gè)詞為“阿拉伯”,那么“阿拉伯語(yǔ)”壓縮為<3,語(yǔ)>。其次大量用到的是對數字的壓縮,數字只保存與上一個(gè)值的差值(這樣可以減小數字的長(cháng)度,進(jìn)而減少保存該數字需要的字節數)。例如當前文章號是16389(不壓縮要用3個(gè)字節保存),上一文章號是16382,壓縮后保存7(只用一個(gè)字節)。
下面我們可以通過(guò)對該索引的查詢(xún)來(lái)解釋一下為什么要建立索引。
假設要查詢(xún)單詞 “live”,lucene先對詞典二元查找、找到該詞,通過(guò)指向頻率文件的指針讀出所有文章號,然后返回結果。詞典通常非常小,因而,整個(gè)過(guò)程的時(shí)間是毫秒級的。
而用普通的順序匹配算法,不建索引,而是對所有文章的內容進(jìn)行字符串匹配,這個(gè)過(guò)程將會(huì )相當緩慢,當文章數目很大時(shí),時(shí)間往往是無(wú)法忍受的。
score_d = sum_t(tf_q * idf_t / norm_q * tf_d * idf_t / norm_d_t * boost_t) * coord_q_d
注解:
score_d : 該文檔d的得分
sum_t : 所有項得分的總和
tf_q : 查詢(xún)串q中,某個(gè)項出項的次數的平方根
tf_d : 文檔d中 ,出現某個(gè)項的次數的平方根
numDocs : 在這個(gè)索引里,找到分數大于0的文檔的總數
docFreq_t : 包含項t的文檔總數
idf_t : log(numDocs/docFreq+1)+1.0
norm_q : sqrt(sum_t((tf_q*idf_t)^2))
norm_d_t : 在文檔d中,與項t相同的域中,所有的項總數的平方根
boost_t : 項t的提升因子,一般為 1.0
coord_q_d : 在文檔d中,命中的項數量除以查詢(xún)q的項總數
luncene對Document和Field提供了一個(gè)可以設置的Boosting參數, 這個(gè)參數的用處是告訴lucene, 某些記錄更重要,在搜索的時(shí)候優(yōu)先考慮他們 比如在搜索的時(shí)候你可能覺(jué)得幾個(gè)門(mén)戶(hù)的網(wǎng)頁(yè)要比垃圾小站更優(yōu)先考慮
lucene默認的boosting參數是1.0, 如果你覺(jué)得這個(gè)field重要,你可以把boosting設置為1.5, 1.2....等, 對Document設置boosting相當設定了它的每個(gè)Field的基準boosting,到時(shí)候實(shí)際Field的boosting就是(Document-boosting*Field-boosting)設置了一遍相同的boosting.
似乎在lucene的記分公式里面有boosting參數,不過(guò)我估計一般人是不會(huì )去研究他的公式的(復雜),而且公式也無(wú)法給出最佳值,所以我們所能做的只能是一點(diǎn)一點(diǎn)的改變boosting, 然后在實(shí)際檢測中觀(guān)察它對搜索結果起到多大的作用來(lái)調整
一般的情況下是沒(méi)有必要使用boosting的, 因為搞不好你就把搜索給搞亂了, 另外如果是單獨對Field來(lái)做Bossting, 也可以通過(guò)將這個(gè)Field提前來(lái)起到近似的效果
日期是lucene需要特殊考慮的地方之一, 因為我們可能需要對日期進(jìn)行范圍搜索, Field.keyword(string,Date)提供了這樣的方法,lucene會(huì )把這個(gè)日期轉換為string, 值得注意的是這里的日期是精確到毫秒的,可能會(huì )有不必要的性能損失, 所以我們也可以把日期自行轉化為YYYYMMDD這樣的形勢,就不用精確到具體時(shí)間了,通過(guò)File.keyword(Stirng,String) 來(lái)index, 使用PrefixQuery 的YYYY一樣能起到簡(jiǎn)化版的日期范圍搜索(小技巧), lucene提到他不能處理1970年以前的時(shí)間,似乎是上一代電腦系統遺留下來(lái)的毛病
如果數字只是簡(jiǎn)單的數據, 比如中國有56個(gè)民族. 那么可以簡(jiǎn)單的把它當字符處理
如果數字還包含數值的意義,比如價(jià)格, 我們會(huì )有范圍搜索的需要(20元到30元之間的商品),那么我們必須做點(diǎn)小技巧, 比如把3,34,100 這三個(gè)數字轉化為003,034,100 ,因為這樣處理以后, 按照字符排序和按照數值排序是一樣的,而lucene內部按照字符排序,003->034->100 NOT(100->3->34)
Lucene默認按照相關(guān)度(score)排序,為了能支持其他的排序方式,比如日期,我們在add Field的時(shí)候,必須保證field被Index且不能被tokenized(分詞),并且排序的只能是數字,日期,字符三種類(lèi)型之一
IndexWriter提供了一些參數可供設置,列表如下
| | 屬性 | 默認值 | 說(shuō)明 |
| mergeFactor | org.apache.lucene.mergeFactor | 10 | 控制index的大小和頻率,兩個(gè)作用 |
| maxMergeDocs | org.apache.lucene.maxMergeDocs | Integer.MAX_VALUE | 限制一個(gè)段中的document數目 |
| minMergeDocs | org.apache.lucene.minMergeDocs | 10 | 緩存在內存中的document數目,超過(guò)他以后會(huì )寫(xiě)入到磁盤(pán) |
| maxFieldLength | | 1000 | 一個(gè)Field中最大Term數目,超過(guò)部分忽略,不會(huì )index到field中,所以自然也就搜索不到 |
這些參數的的詳細說(shuō)明比較復雜:mergeFactor有雙重作用
設置每mergeFactor個(gè)document寫(xiě)入一個(gè)段,比如每10個(gè)document寫(xiě)入一個(gè)段
設置每mergeFacotr個(gè)小段合并到一個(gè)大段,比如10個(gè)document的時(shí)候合并為1小段,以后有10個(gè)小段以后合并到一個(gè)大段,有10個(gè)大段以后再合并,實(shí)際的document數目會(huì )是mergeFactor的指數
簡(jiǎn)單的來(lái)說(shuō)mergeFactor 越大,系統會(huì )用更多的內存,更少磁盤(pán)處理,如果要打批量的作index,那么把mergeFactor設置大沒(méi)錯, mergeFactor 小了以后, index數目也會(huì )增多,searhing的效率會(huì )降低, 但是mergeFactor增大一點(diǎn)一點(diǎn),內存消耗會(huì )增大很多(指數關(guān)系),所以要留意不要"out of memory"
把maxMergeDocs設置小,可以強制讓達到一定數量的document寫(xiě)為一個(gè)段,這樣可以抵消部分mergeFactor的作用.
minMergeDocs相當于設置一個(gè)小的cache,第一個(gè)這個(gè)數目的document會(huì )留在內存里面,不寫(xiě)入磁盤(pán)。這些參數同樣是沒(méi)有最佳值的, 必須根據實(shí)際情況一點(diǎn)點(diǎn)調整。
maxFieldLength可以在任何時(shí)刻設置, 設置后,接下來(lái)的index的Field會(huì )按照新的length截取,之前已經(jīng)index的部分不會(huì )改變??梢栽O置為Integer.MAX_VALUE
RAMDirectory(RAMD)在效率上比FSDirectyr(FSD)高不少, 所以我們可以手動(dòng)的把RAMD當作FSD的buffer,這樣就不用去很費勁的調優(yōu)FSD那么多參數了,完全可以先用RAM跑好了index, 周期性(或者是別的什么算法)來(lái)回寫(xiě)道FSD中。 RAMD完全可以做FSD的buffer。
Indexwriter.optimize()方法可以為查詢(xún)優(yōu)化索引(index),之前提到的參數調優(yōu)是為indexing過(guò)程本身優(yōu)化,而這里是為查詢(xún)優(yōu)化,優(yōu)化主要是減少index文件數,這樣讓查詢(xún)的時(shí)候少打開(kāi)文件,優(yōu)化過(guò)程中,lucene會(huì )拷貝舊的index再合并,合并完成以后刪除舊的index,所以在此期間,磁盤(pán)占用增加, IO符合也會(huì )增加,在優(yōu)化完成瞬間,磁盤(pán)占用會(huì )是優(yōu)化前的2倍,在optimize過(guò)程中可以同時(shí)作search。
<!--[if !supportLists]-->v <!--[endif]-->所有只讀操作都可以并發(fā)
<!--[if !supportLists]-->v <!--[endif]-->在index被修改期間,所有只讀操作都可以并發(fā)
<!--[if !supportLists]-->v <!--[endif]-->對index修改操作不能并發(fā),一個(gè)index只能被一個(gè)線(xiàn)程占用
<!--[if !supportLists]-->v <!--[endif]-->index的優(yōu)化,合并,添加都是修改操作
<!--[if !supportLists]-->v <!--[endif]-->IndexWriter和IndexReader的實(shí)例可以被多線(xiàn)程共享,他們內部是實(shí)現了同步,所以外面使用不需要同步
lucence內部使用文件來(lái)locking, 默認的locking文件放在java.io.tmpdir,可以通過(guò)-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock兩個(gè)文件,lock文件用來(lái)防止并行操作index,如果并行操作, lucene會(huì )拋出異常,可以通過(guò)設置-DdisableLuceneLocks=true來(lái)禁止locking,這樣做一般來(lái)說(shuō)很危險,除非你有操作系統或者物理級別的只讀保證,比如把index文件刻盤(pán)到CDROM上。
Lucene中最基礎的概念是索引(index),文檔(document.,域(field)和項(term)。
索引包含了一個(gè)文檔的序列。
· 文檔是一些域的序列。
· 域是一些項的序列。
· 項就是一個(gè)字串。
存在于不同域中的同一個(gè)字串被認為是不同的項。因此項實(shí)際是用一對字串表示的,第一個(gè)字串是域名,第二個(gè)是域中的字串。
Lucene中,域的文本可能以逐字的非倒排的方式存儲在索引中。而倒排過(guò)的域稱(chēng)為被索引過(guò)了。域也可能同時(shí)被存儲和被索引。
域的文本可能被分解許多項目而被索引,或者就被用作一個(gè)項目而被索引。大多數的域是被分解過(guò)的,但是有些時(shí)候某些標識符域被當做一個(gè)項目索引是很有用的。
Lucene索引可能由多個(gè)子索引組成,這些子索引成為段。每一段都是完整獨立的索引,能被搜索。索引是這樣作成的:
1. 為新加入的文檔創(chuàng )建新段。
2. 合并已經(jīng)存在的段。
搜索時(shí)需要涉及到多個(gè)段和/或者多個(gè)索引,每一個(gè)索引又可能由一些段組成。
內部的來(lái)說(shuō),Lucene用一個(gè)整形(interger)的文檔號來(lái)指示文檔。第一個(gè)被加入到索引中的文檔就是0號,順序加入的文檔將得到一個(gè)由前一個(gè)號碼遞增而來(lái)的號碼。
注意文檔號是可能改變的,所以在Lucene外部存儲這些號碼時(shí)必須小心。特別的,號碼的改變的情況如下:
· 只 有段內的號碼是相同的,不同段之間不同,因而在一個(gè)比段廣泛的上下文環(huán)境中使用這些號碼時(shí),就必須改變它們。標準的技術(shù)是根據每一段號碼多少為每一段分配 一個(gè)段號。將段內文檔號轉換到段外時(shí),加上段號。將某段外的文檔號轉換到段內時(shí),根據每段中可能的轉換后號碼范圍來(lái)判斷文檔屬于那一段,并減調這一段的段 號。例如有兩個(gè)含5個(gè)文檔的段合并,那么第一段的段號就是0,第二段段號5。第二段中的第三個(gè)文檔,在段外的號碼就是8。
· 文檔刪除后,連續的號碼就出現了間斷。這可以通過(guò)合并索引來(lái)解決,段合并時(shí)刪除的文檔相應也刪掉了,新合并而成的段并沒(méi)有號碼間斷。
索引段維護著(zhù)以下的信息:
· 域集合。包含了索引中用到的所有的域。
· 域值存儲表。每一個(gè)文檔都含有一個(gè)“屬性-值”對的列表,屬性即為域名。這個(gè)列表用來(lái)存儲文檔的一些附加信息,如標題,url或者訪(fǎng)問(wèn)數據庫的一個(gè)ID。在搜索時(shí)存儲域的集合可以被返回。這個(gè)表以文檔號標識。
· 項字典。這個(gè)字典含有所有文檔的所有域中使用過(guò)的的項,同時(shí)含有使用過(guò)它的文檔的文檔號,以及指向使用頻數信息和位置信息的指針。
· 項頻數信息。對于項字典中的每個(gè)項,這些信息包含含有這個(gè)項的文檔的總數,以及每個(gè)文檔中使用的次數。
· 項位置信息。對于項字典中的每個(gè)項,都存有在每個(gè)文檔中出現的各個(gè)位置。
· 標準化因子。對于文檔中的每一個(gè)域,存有一個(gè)值,用來(lái)以后乘以這個(gè)這個(gè)域的命中數(hits)。
· 被刪除的文檔信息。這是一個(gè)可選文件,用來(lái)表明那些文檔已經(jīng)刪除了。
接下來(lái)的各部分部分詳細描述這些信息。
同屬于一個(gè)段的文件擁有相同的文件名,不同的擴展名。擴展名由以下討論的各種文件格式確定。
一般來(lái)說(shuō),一個(gè)索引存放一個(gè)目錄,其所有段都存放在這個(gè)目錄里,不這樣作,也是可以的,在性能方面較低。
最基本的數據類(lèi)型就是字節(byte,8位)。文件就是按字節順序訪(fǎng)問(wèn)的。其它的一些數據類(lèi)型也定義為字節的序列,文件的格式具有字節意義上的獨立性。
UInt32 :32位無(wú)符號整數,由四個(gè)字節組成,高位優(yōu)先。UInt32 --> <Byte>4
Uint64 : 64位無(wú)符號整數,由八字節組成,高位優(yōu)先。UInt64 --> <Byte>8
VInt : 可變長(cháng)的正整數類(lèi)型,每字節的最高位表明還剩多少字節。每字節的低七位表明整數的值。因此單字節的值從0到127,兩字節值從128到16,383,等等。
VInt 編碼示例
value
First byte
Second byte
Third byte
0
00000000
1
00000001
2
00000010
...
127
01111111
128
10000000
00000001
129
10000001
00000001
130
10000010
00000001
...
16,383
11111111
01111111
16,384
10000000
10000000
00000001
16,385
10000001
10000000
00000001
... 這種編碼提供了一種在高效率解碼時(shí)壓縮數據的方法。
Lucene輸出UNICODE字符序列,使用標準UTF-8編碼。
String :Lucene輸出由VINT和字符串組成的字串,VINT表示字串長(cháng),字符串緊接其后。
String --> VInt, Chars
索引中活動(dòng)的段存儲在Segments文件中。每個(gè)索引只能含有一個(gè)這樣的文件,名為"segments".這個(gè)文件依次列出每個(gè)段的名字和每個(gè)段的大小。
Segments --> SegCount, <SegName, SegSize>SegCount
SegCount, SegSize --> UInt32
SegName --> String
SegName表示該segment的名字,同時(shí)作為索引其他文件的前綴。
SegSize是段索引中含有的文檔數。
有一些文件用來(lái)表示另一個(gè)進(jìn)程在使用索引。
· 如果存在"commit.lock"文件,表示有進(jìn)程在寫(xiě)"segments"文件和刪除無(wú)用的段索引文件,或者表示有進(jìn)程在讀"segments"文件和打開(kāi)某些段的文件。在一個(gè)進(jìn)程在讀取"segments"文件段信息后,還沒(méi)來(lái)得及打開(kāi)所有該段的文件前,這個(gè)Lock文件可以防止另一個(gè)進(jìn)程刪除這些文件。
· 如果存在"index.lock"文件,表示有進(jìn)程在向索引中加入文檔,或者是從索引中刪除文檔。這個(gè)文件防止很多文件同時(shí)修改一個(gè)索引。
名為"deletetable"的文件包含了索引不再使用的文件的名字,這些文件可能并沒(méi)有被實(shí)際的刪除。這種情況只存在與Win32平臺下,因為Win32下文件仍打開(kāi)時(shí)并不能刪除。
Deleteable --> DelableCount, <DelableName>DelableCount
DelableCount --> UInt32
DelableName --> String
剩下的文件是每段中包含的文件,因此由后綴來(lái)區分。
域(Field)
域集合信息(Field Info)
所有域名都存儲在這個(gè)文件的域集合信息中,這個(gè)文件以后綴.fnm結尾。
FieldInfos (.fnm) --> FieldsCount, <FieldName, FieldBits>FieldsCount
FieldsCount --> VInt
FieldName --> String
FieldBits --> Byte
目前情況下,FieldBits只有使用低位,對于已索引的域值為1,對未索引的域值為0。
文件中的域根據它們的次序編號。因此域0是文件中的第一個(gè)域,域1是接下來(lái)的,等等。這個(gè)和文檔號的編號方式相同。
域值存儲表使用兩個(gè)文件表示:
1. 域索引(.fdx文件)。
如下,對于每個(gè)文檔這個(gè)文件包含指向域值的指針:
FieldIndex (.fdx) --> <FieldvaluesPosition>SegSize
FieldvaluesPosition --> Uint64
FieldvaluesPosition指示的是某一文檔的某域的域值在域值文件中的位置。因為域值文件含有定長(cháng)的數據信息,因而很容易隨機訪(fǎng)問(wèn)。在域值文件中,文檔n的域值信息就存在n*8位置處(The position of document.nbspn‘s field data is the Uint64 at n*8 in this file.)。
2. 域值(.fdt文件)。
如下,每個(gè)文檔的域值信息包含:
FieldData (.fdt) --> <DocFieldData>SegSize
DocFieldData --> FieldCount, <FieldNum, Bits, value>FieldCount
FieldCount --> VInt
FieldNum --> VInt
Bits --> Byte
value --> String
目前情況下,Bits只有低位被使用,值為1表示域名被分解過(guò),值為0表示未分解過(guò)。÷
項字典用以下兩個(gè)文件表示:
1. 項信息(.tis文件)。
TermInfoFile (.tis)--> TermCount, TermInfos
TermCount --> UInt32
TermInfos --> <TermInfo>TermCount
TermInfo --> <Term, DocFreq, FreqDelta, ProxDelta>
Term --> <PrefixLength, Suffix, FieldNum>
Suffix --> String
PrefixLength, DocFreq, FreqDelta, ProxDelta
--> VInt
項信息按項排序。項信息排序時(shí)先按項所屬的域的文字順序排序,然后按照項的字串的文字順序排序。
項的字前綴往往是共同的,與字的后綴組成字。PrefixLength變量就是表示與前一項相同的前綴的字數。因此,如果前一個(gè)項的字是"bone",后一個(gè)是"boy"的話(huà),PrefixLength值為2,Suffix值為"y"。
FieldNum指明了項屬于的域號,而域名存儲在.fdt文件中。
DocFreg表示的是含有該項的文檔的數量。
FreqDelta指明了項所屬TermFreq變量在.frq文件中的位置。詳細的說(shuō),就是指相對于前一個(gè)項的數據的位置偏移量(或者是0,表示文件中第一個(gè)項)。
ProxDelta指明了項所屬的TermPosition變量在.prx文件中的位置。詳細的說(shuō),就是指相對于前一個(gè)項的數據的位置偏移量(或者是0,表示文件中第一個(gè)項)。
2. 項信息索引(.tii文件)。
每個(gè)項信息索引文件包含.tis文件中的128個(gè)條目,依照條目在.tis文件中的順序。這樣設計是為了一次將索引信息讀入內存能,然后使用它來(lái)隨機的訪(fǎng)問(wèn).tis文件。
這個(gè)文件的結構和.tis文件非常類(lèi)似,只在每個(gè)條目記錄上增加了一個(gè)變量IndexDelta。
TermInfoIndex (.tii)--> IndexTermCount, TermIndices
IndexTermCount --> UInt32
TermIndices --> <TermInfo, IndexDelta>IndexTermCount
IndexDelta --> VInt
IndexDelta表示該項的TermInfo變量值在.tis文件中的位置。詳細的講,就是指相對于前一個(gè)條目的偏移量(或者是0,對于文件中第一個(gè)項)。
.frq文件包含每一項的文檔的列表,還有該項在對應文檔中出現的頻數。
FreqFile (.frq) --> <TermFreqs>TermCount
TermFreqs --> <TermFreq>DocFreq
TermFreq --> DocDelta, Freq?
DocDelta,Freq --> VInt
TermFreqs序列按照項來(lái)排序(依據于.tis文件中的項,即項是隱含存在的)。
TermFreq元組按照文檔號升序排列。
DocDelta決定了文檔號和頻數。詳細的說(shuō),DocDelta/2表示相對于前一文檔號的偏移量(或者是0,表示這是TermFreqs里面的第一項)。當DocDelta是奇數時(shí)表示在該文檔中頻數為1,當DocDelta是偶數時(shí),另一個(gè)VInt(Freq)就表示在該文檔中出現的頻數。
例如,假設某一項在文檔7中出現一次,在文檔11中出現了3次,在TermFreqs中就存在如下的VInts序列:
15, 22, 3
.prx文件包含了某文檔中某項出現的位置信息的列表。
ProxFile (.prx) --> <TermPositions>TermCount
TermPositions --> <Positions>DocFreq
Positions --> <PositionDelta>Freq
PositionDelta --> VInt
TermPositions按照項來(lái)排序(依據于.tis文件中的項,即項是隱含存在的)。
Positions元組按照文檔號升序排列。
PositionDelta是相對于前一個(gè)出現位置的偏移位置(或者為0,表示這是第一次在這個(gè)文檔中出現)。
例如,假設某一項在某文檔第4項出現,在另一個(gè)文檔中第5項和第9項出現,將存在如下的VInt序列:
4, 5, 4
.nrm文件包含了每個(gè)文檔的標準化因子,標準化因子用來(lái)以后乘以這個(gè)這個(gè)域的命中數。
Norms (.nrm) --> <Byte>SegSize
每個(gè)字節記錄一個(gè)浮點(diǎn)數。位0-2包含了3位的尾數部分,位3-8包含了5位的指數部分。
按如下規則可將這些字節轉換為IEEE標準單精度浮點(diǎn)數:
1. 如果該字節是0,就是浮點(diǎn)0;
2. 否則,設置新浮點(diǎn)數的標志位為0;
3. 將字節中的指數加上48后作為新的浮點(diǎn)數的指數;
4. 將字節中的尾數映射到新浮點(diǎn)數尾數的高3位;并且
5. 設置新浮點(diǎn)數尾數的低21位為0。
.del文件是可選的,只有在某段中存在刪除操作后才存在:
Deletions (.del) --> ByteCount,BitCount,Bits
ByteSize,BitCount --> Uint32
Bits --> <Byte>ByteCount
ByteCount表示的是Bits列表中Byte的數量。典型的,它等于(SegSize/8)+1。
BitCount表示Bits列表中多少個(gè)已經(jīng)被設置過(guò)了。
Bits列表包含了一些位(bit),順序表示一個(gè)文檔。當對應于文檔號的位被設置了,就標志著(zhù)這個(gè)文檔已經(jīng)被刪除了。位的順序是從低到高。因此,如果Bits包含兩個(gè)字節,0x00和0x02,那么表示文檔9已經(jīng)刪除了。
在以上的文件格式中,好幾處都有限制項和文檔的最大個(gè)數為32位數的極限,即接近于40億。今天看來(lái),這不會(huì )造成問(wèn)題,但是,長(cháng)遠的看,可能造成問(wèn)題。因此,這些極限應該或者換為UInt64類(lèi)型的值,或者更好的,換為VInt類(lèi)型的值(VInt值沒(méi)有上限)。
有兩處地方的代碼要求必須是定長(cháng)的值,他們是:
1. FieldvaluesPosition變量(存儲于域索引文件中,.fdx文件)。它已經(jīng)是一個(gè)UInt64型,所以不會(huì )有問(wèn)題。
2. TermCount變量(存儲于項信息文件中,.tis文件)。這是最后輸出到文件中的,但是最先被讀取,因此是存儲于文件的最前端 。索引代碼先在這里寫(xiě)入一個(gè)0值,然后在其他文件輸出完畢后覆蓋這個(gè)值。所以無(wú)論它存儲在什么地方,它都必須是一個(gè)定長(cháng)的值,它應該被變成UInt64型。
除此之外,所有的UInt值都可以換成VInt型以去掉限制。
| 應用情景分析 | |||
| Query query = parser.parse(queries[j]); | 獲得布爾查詢(xún) | ||
| hits = searcher.search(query); | | ||
| return new Hits(this, query, filter); | |||
| getMoreDocs(50) | |||
| TopDocs topDocs = searcher.search(query, filter, n) | |||
| IndexSearcher:public TopDocs search(Query query, Filter filter, final int nDocs) <!--[if !supportLists]-->² <!--[endif]-->IndexSearcher 開(kāi)始時(shí)已經(jīng)打開(kāi)了該目錄 <!--[if !supportLists]-->² <!--[endif]-->IndexSearcher 中初始化了IndexReader <!--[if !supportLists]-->² <!--[endif]-->IndexReader中讀取了SegmentInfos <!--[if !supportLists]-->² <!--[endif]-->IndexReader = SegmentReader <!--[if !supportLists]-->² <!--[endif]-->SegmentReader ::initialize(SegmentInfo si) <!--[if !supportLists]-->n <!--[endif]-->1。讀入域信息,只有域的名字 <!--[if !supportLists]-->n <!--[endif]-->2. 打開(kāi)保存域、保存域索引的文件 | |||
| Scorer scorer = query.weight(this).scorer(reader) <!--[if !supportLists]-->u <!--[endif]-->這里query = PhraseQuery <!--[if !supportLists]-->u <!--[endif]-->query.weight(this) 獲得PhraseWeight(IndexSearcher) <!--[if !supportLists]-->u <!--[endif]-->PhraseWeight::scorer(IndexReader reader) <!--[if !supportLists]-->u <!--[endif]-->PhraseQuery::TermPositions p = reader.termPositions((Term)terms.elementAt(i)); <!--[if !supportLists]-->u <!--[endif]-->public TermPositions termPositions(Term term) throws IOException { IndexReader::TermPositions termPositions = termPositions();
IndexReader = SegmentReader, IndexSearcher termPositions.seek(term);
return termPositions; <!--[if !supportLists]-->² <!--[endif]-->SegmentReader. termPositions()::return SegmentTermPositions(this)` | |||
| <p>一個(gè)權重由query創(chuàng )建,并給查詢(xún)器({@link Query#createWeight(Searcher)})使用,方法 {@link #sumOfSquaredWeights()},然后被最高級的查詢(xún)api調用 用來(lái)計算查詢(xún)規范化因子 (@link Similarity#queryNorm(float)}),然后該因子傳給{@link #normalize(float)} 然后被{@link #scorer(IndexReader)}調用 | |||
| | |||
應用情景分析
| 規則: 加粗體的黑色代碼,表示將作深入分析 try { Directory directory = new RAMDirectory(); Analyzer analyzer = new SimpleAnalyzer(); IndexWriter writer = new IndexWriter(directory, analyzer, true); String[] docs = { "a b c d e", "a b c d e a b c d e", "a b c d e f g h i j", "a c e", "e c a", "a c e a c e", "a c e a b c" }; for (int j = 0; j < docs.length; j++) { Document d = new Document(); d.add(Field.Text("contents", docs[j])); writer.addDocument(d); } writer.close(); 以上代碼是準備工作,生成索引 Searcher searcher = new IndexSearcher(directory); 以上代碼,初始化查詢(xún),分析編號1。1 String[] queries = {"\"a c e\"", }; Hits hits = null; QueryParser parser = new QueryParser("contents", analyzer); parser.setPhraseSlop(0); for (int j = 0; j < queries.length; j++) { Query query = parser.parse(queries[j]); 該Query = PhraseQuery System.out.println("Query: " + query.toString("contents")); hits = searcher.search(query); 以上代碼,初始化查詢(xún),分析編號1。2 System.out.println(hits.length() + " total results"); for (int i = 0 ; i < hits.length() && i < 10; i++) { Document d = hits.doc(i); System.out.println(i + " " + hits.score(i) // + " " + DateField.stringToDate(d.get("modified")) + " " + d.get("contents")); } } searcher.close(); } catch (Exception e) { System.out.println(" caught a " + e.getClass() + "\n with message: " + e.getMessage()); } |
| 查詢(xún)結果: Query: "a c e" 3 total results 0 1 2 |
通過(guò)目錄,創(chuàng )建一個(gè)索引搜索器,
調用類(lèi)
IndexSearcher ::public IndexSearcher(Directory directory) throws IOException {
this(IndexReader.open(directory), true);
}
調用
private IndexSearcher(IndexReader r, boolean closeReader) {
reader = r;
this.closeReader = closeReader;
}
調用
private static IndexReader open(final Directory directory, final boolean closeDirectory) throws IOException {
synchronized (directory) { // in- & inter-process sync
return (IndexReader)new Lock.With(
directory.makeLock(IndexWriter.COMMIT_LOCK_NAME),
IndexWriter.COMMIT_LOCK_TIMEOUT) {
public Object doBody() throws IOException {
SegmentInfos infos = new SegmentInfos();
從目錄中讀取SegmentInfos
infos.read(directory);
if (infos.size() == 1) { // index is optimized
return new SegmentReader(infos, infos.info(0), closeDirectory);
} else {
IndexReader[] readers = new IndexReader[infos.size()];
for (int i = 0; i < infos.size(); i++)
readers[i] = new SegmentReader(infos.info(i));
return new MultiReader(directory, infos, closeDirectory, readers);
}
}
}.run();
}
}
代碼到這里,已經(jīng)讀取了文件segments文件,獲得段信息,該測試只有一個(gè)段,所以執行了return new SegmentReader(infos, infos.info(0), closeDirectory);,記住IndexReader = SegmentReader
infos.read(directory):
/** 讀取輸入參數的目錄,下的segments文件
* 代碼分析:
* 1。讀取格式,小于0表示該文件有隱含的格式信息,小于-1就表示該格式是未知的,因為最小的格式是-1
* 2。小于0時(shí),再讀取版本信息以及段的計數
* 3。大于0,表示segments文件開(kāi)頭部分沒(méi)有版本信息,只有段的計數
* 4。讀取段的數量
* 5。循環(huán)讀取段信息,然后構建段信息對象,最后把這些對象都加入到段集合中
* 6。大于0時(shí),判斷是否文件最后有版本信息,有的話(huà)就賦值version,沒(méi)有的話(huà),version =0 */,該段代碼比較簡(jiǎn)單,讀者可以從看src中代碼
return new SegmentReader(infos, infos.info(0), closeDirectory);
SegmentReader(SegmentInfos sis, SegmentInfo si, boolean closeDir)
throws IOException {
super(si.dir, sis, closeDir);
initialize(si);
}
super(si.dir, sis, closeDir);
IndexReader ::IndexReader(Directory directory, SegmentInfos segmentInfos, boolean closeDirectory) {
this.directory = directory;
this.segmentInfos = segmentInfos;
directoryOwner = true;
this.closeDirectory = closeDirectory;
stale = false;
hasChanges = false;
writeLock = null;
}
SegmentReader ::initialize(si);
/** 初始化這個(gè)段信息
該段代碼是初始化了
* 1。讀入域信息,只有域的名字
* 2. 打開(kāi)保存域、保存域索引的文件
*/
private void initialize(SegmentInfo si) throws IOException
{
segment = si.name;
// Use compound file directory for some files, if it exists
Directory cfsDir = directory();// 就是保存該段的目錄
// CompoundFileReader(組合文件讀取器)也是(目錄)的子類(lèi)
if (directory().fileExists(segment + ".cfs")) {
cfsReader = new CompoundFileReader(directory(), segment + ".cfs");
cfsDir = cfsReader;
}
// 1。讀入域信息,只有域的名字
fieldInfos = new FieldInfos(cfsDir, segment + ".fnm"); // 這個(gè)過(guò)程讀入所有的域信息了
// 2。打開(kāi)保存域、保存域索引的文件
fieldsReader = new FieldsReader(cfsDir, segment, fieldInfos);
tis = new TermInfosReader(cfsDir, segment, fieldInfos);
if (hasDeletions(si))
deletedDocs = new BitVector(directory(), segment + ".del");// 讀入刪除表
freqStream = cfsDir.openFile(segment + ".frq");// 讀入頻率文件
proxStream = cfsDir.openFile(segment + ".prx");// 讀入位置文件
openNorms(cfsDir);// 讀入文件segment.f1,segment.f2……,建立hashtable
if (fieldInfos.hasVectors()) { // open term vector files only as needed
termVectorsReader = new TermVectorsReader(cfsDir, segment, fieldInfos);
}
}
這時(shí),searcher = IndexSearcher,對該代碼的跟蹤如下:
調用:return search(query, (Filter)null)
調用:return new Hits(this, query, filter);
調用:Hit::Hits(Searcher s, Query q, Filter f) throws IOException {
query = q;
searcher = s;
filter = f;
getMoreDocs(50); // retrieve 100 initially
}
getMoreDocs(int min)調用::TopDocs topDocs = searcher.search(query, filter, n)
searcher.search(query, filter, n) 調用Scorer scorer = query.weight(this).scorer(reader);
IndexSearcher::public TopDocs search(Query query, Filter filter, final int nDocs)
throws IOException {
Scorer scorer = query.weight(this).scorer(reader);
if (scorer == null)
return new TopDocs(0, new ScoreDoc[0]);
final BitSet bits = filter != null ? filter.bits(reader) : null;
final HitQueue hq = new HitQueue(nDocs);
final int[] totalHits = new int[1];
scorer.score(new HitCollector() {
private float minScore =
public final void collect(int doc, float score) {
if (score >
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (hq.size() < nDocs || score >= minScore) {
hq.insert(new ScoreDoc(doc, score));
minScore = ((ScoreDoc)hq.top()).score; // maintain minScore
}
}
}
});
ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()];
for (int i = hq.size()-1; i >= 0; i--) // put docs in array
scoreDocs[i] = (ScoreDoc)hq.pop();
return new TopDocs(totalHits[0], scoreDocs);
}
參數分析:query = PhraseQuery (該參數由主測試程序中的Query query = parser.parse(queries[j]);初始化)
this = IndexSearcher(該參數初始化,已經(jīng)初始化了主要的文件,具體可參考1.1)
由代碼
| 1 PhraseQuery:: protected Weight createWeight(Searcher searcher) { if (terms.size() == 1) { // optimize one-term case Term term = (Term)terms.elementAt(0); Query termQuery = new TermQuery(term); termQuery.setBoost(getBoost()); return termQuery.createWeight(searcher); } return new PhraseWeight(searcher); } |
query.weight(this) 創(chuàng )建了PhraseWeight(searcher)
Scorer scorer = query.weight(this).scorer(reader) 就相當于PhraseWeight(searcher). .scorer(reader),即調用以下代碼:
| 2 PhraseQuery:: public Scorer scorer(IndexReader reader) throws IOException { if (terms.size() == 0) // optimize zero-term case return null; // 讀取項的 位置信息 TermPositions[] tps = new TermPositions[terms.size()]; for (int i = 0; i < terms.size(); i++) { TermPositions p = reader.termPositions((Term)terms.elementAt(i)); if (p == null) return null; tps[i] = p; } 得到所有項的項信息,TermPositions[ ] =SegmentTermPositions[ ] if (slop == 0) // optimize exact case return new ExactPhraseScorer(this, tps, getPositions(), getSimilarity(searcher), reader.norms(field)); } |
<!--[if !supportLists]-->ü <!--[endif]-->TermPositions p = reader.termPositions((Term)terms.elementAt(i));
這時(shí)Term文本為查詢(xún)里的項
public TermPositions termPositions(Term term) throws IOException {
TermPositions termPositions = termPositions();
termPositions.seek(term);
return termPositions;
}
termPositions()::
SegmentReader ::public final TermPositions termPositions() throws IOException {
return new SegmentTermPositions(this);
}
parent =SegmentReader,即剛才的段讀取器
tis = new TermInfosReader(cfsDir, segment, fieldInfos); 即項信息讀取器
SegmentTermPositions(this)::
SegmentTermPositions ::SegmentTermPositions(SegmentReader p) throws IOException {
super(p);
this.proxStream = (InputStream)parent.proxStream.clone();
}
super(p)::
SegmentTermDocs(SegmentReader parent)
throws IOException {
this.parent = parent;
this.freqStream = (InputStream) parent.freqStream.clone();
this.deletedDocs = parent.deletedDocs;
this.skipInterval = parent.tis.getSkipInterval();
}
termPositions.seek(term);
public void seek(Term term) throws IOException {
根據項,從項信息讀取器中讀取對應的項信息,該方法是線(xiàn)程安全的
TermInfo ti = parent.tis.get(term);
seek(ti);
}
seek(TermInfo ti)
SegmentTermDocs的項信息轉變?yōu)楝F在讀入的項的信息
void seek(TermInfo ti) throws IOException {
count = 0;
if (ti == null) {
df = 0;
} else {
df = ti.docFreq;
doc = 0;
skipDoc = 0;
skipCount = 0;
numSkips = df / skipInterval;
freqPointer = ti.freqPointer;
proxPointer = ti.proxPointer;
skipPointer = freqPointer + ti.skipOffset;
freqStream.seek(freqPointer);
haveSkipped = false;
}
}
new ExactPhraseScorer(this, tps, getPositions(), getSimilarity(searcher),reader.norms(field));
調用構造器
ExactPhraseScorer(Weight weight, TermPositions[] tps, int[] positions, Similarity similarity,
byte[] norms) throws IOException {
super(weight, tps, positions, similarity, norms);
調用超類(lèi)構造器,獲得短語(yǔ)位置的頻繁度信息和位置信息,并構造一個(gè)優(yōu)先隊列
PhraseScorer(Weight weight, TermPositions[] tps, int[] positions, Similarity similarity,
byte[] norms) {
super(similarity);
this.norms = norms;
this.weight = weight;
this.value = weight.getValue();
// convert tps to a list
// 把 PhrasePositions 放在一個(gè)一般的隊列里面(以鏈表形式)
for (int i = 0; i < tps.length; i++) {
PhrasePositions pp = new PhrasePositions(tps[i], positions[i]);
if (last != null) { // add next to end of list
last.next = pp;
} else
first = pp;
last = pp;
}
pq = new PhraseQueue(tps.length); // construct empty pq
}
使用該記分器記分,并收集
scorer.score(new HitCollector()
public void score(HitCollector hc) throws IOException {
while (next()) {
hc.collect(doc(), score());
}
}
hc.collect(doc(), score());
score()調用,value為權值
PhraseScorer::public float score() throws IOException {
//System.out.println("scoring " + first.doc);
float raw = getSimilarity().tf(freq) * value; // raw score
return raw * Similarity.decodeNorm(norms[first.doc]); // normalize
}
把各個(gè)位置的文檔和得分收集
public final void collect(int doc, float score) {
if (score >
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (hq.size() < nDocs || score >= minScore) {
hq.insert(new ScoreDoc(doc, score));
minScore = ((ScoreDoc)hq.top()).score; // maintain minScore
}
}
}
到這里就出來(lái)了查詢(xún)的文檔和分數,并且這些文檔和分數經(jīng)過(guò)了指定的排序和過(guò)濾
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1454992
聯(lián)系客服