1. 引言
1.1. 編寫目的
介紹開源軟體搜尋引擎——lucene的各個實作的功能,性能,以及代碼分析
1.2. 背景
分析的系統名稱 | Lucene |
該開源首頁 | http://lucene.apache.org/ |
開發語言 | JAVA |
該系統的分析者 | zzpchina |
該系統作者簡介 | Lucene的貢獻者Doug Cutting是一位資深全文索引/檢索專家,曾經是V-Twin搜尋引擎(Apple的Copland作業系統的成就之一)的主要開發者,後在Excite擔任進階系統架構設計師,目前從事于一些INTERNET底層架構的研究。他貢獻出的Lucene的目标是為各種中小型應用程式加入全文檢索功能。 |
該系統簡介 | Lucene不是一個完整的全文索引應用,而是是一個用Java寫的全文索引引擎工具包,它可以友善的嵌入到各種應用中實作針對應用的全文索引/檢索功能。 |
1.3. 該項目的使用現狀
經過多年的發展,Lucene在全文檢索領域已經有了很多的成功案例,并積累了良好的聲譽。
基于Lucene的全文檢索産品和應用Lucene的項目在世界各地已經非常之多,比較知名的有:
1. Eclipse:主流Java開發工具,其幫助文檔采用Lucene作為檢索引擎
2. Jive:知名論壇系統,其檢索功能基于Lucene
3. Ifinder:出自德國的網站檢索系統,基于Lucene(http://ifinder.intrafind.org/)
4. MIT DSpace Federation:一個文檔管理系統(http://www.dspace.org/)
國内外采用Lucene作為網站全文檢索引擎的也很多,比較知名的有:
1. http://www.blogchina.com/weblucene/
2. http://www.ioffer.com/
3. http://search.soufun.com/
4. http://www.taminn.com/
(更多案例,參見http://wiki.apache.org/jakarta-lucene/PoweredBy)
在所有這些案例中,開源應用占了很大一部分,但更多的還是商化業産品和網站。
2. 功能分析
2.1. 與Oracle資料庫對比
Lucene的API接口設計的比較通用,輸入輸出結構都很像資料庫的表==>記錄==>字段,是以很多傳統的應用的檔案、資料庫等都可以比較友善的映射到Lucene的存儲結構/接口中。總體上看:可以先把Lucene當成一個支援全文索引的資料庫系統。
全文檢索庫對關系型資料庫對比 | ||
對比項 | 全文檢索庫(Lucene) | 關系型資料庫(Oracle) |
核心功能 | 以文字檢索為主,插入(insert)、删除(delete)、修改(update)比較麻煩,适合于大文本塊的查詢。 | 插入(insert)、删除(delete)、修改(update)十分友善,有專門的SQL指令,但對于大文本塊(如CLOB)類型的檢索效率低下。 |
庫 | 與Oracle類似,都可以建多個庫,且各個庫的存儲位置可以不同。 | 可以建多個庫,每個庫一般都有控制檔案和資料檔案等,比較複雜。 |
表 | 沒有嚴格的表的概念,比如Lucene的表隻是由入庫時的定義字段松散組成。 | 有嚴格的表結構,有主鍵,有字段類型等。 |
記錄 | 由于沒有嚴格表的概念,是以記錄展現為一個對象,在Lucene裡記錄對應的類是Document。 | Record,與表結構對應。 |
字段 | 字段類型隻有文本和日期兩種,字段一般不支援運算,更無函數功能。 在Lucene裡字段的類是Field,如document(field1,field2…) | 字段類型豐富,功能強大。 record(field1,field2…) |
查詢結果集 | 在Lucene裡表示查詢結果集的類是Hits,如hits(doc1,doc2,doc3…) | 在JDBC為例, Resultset(record1,record2,record3...) |
2.2. Lucene全文搜尋相對資料庫搜尋的優點
全文檢索 ≠ like "%keyword%"
通常比較厚的書籍後面常常附關鍵詞索引表(比如:北京:12, 34頁,上海:3,77頁……),它能夠幫助讀者比較快地找到相關内容的頁碼。而資料庫索引能夠大大提高查詢的速度原理也是一樣,想像一下通過書後面的索引查找的速度要比一頁一頁地翻内容高多少倍……而索引之是以效率高,另外一個原因是它是排好序的。對于檢索系統來說核心是一個排序問題。
由于資料庫索引不是為全文索引設計的,是以,使用like "%keyword%"時,資料庫索引是不起作用的,在使用like查詢時,搜尋過程又變成類似于一頁頁翻書的周遊過程了,是以對于含有模糊查詢的資料庫服務來說,LIKE對性能的危害是極大的。如果是需要對多個關鍵詞進行模糊比對:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
是以建立一個高效檢索系統的關鍵是建立一個類似于科技索引一樣的反向索引機制,将資料源(比如多篇文章)排序順序存儲的同時,有另外一個排好序的關鍵詞清單,用于存儲關鍵詞==>文章映射關系,利用這樣的映射關系索引:
關鍵詞==>出現關鍵詞的文章編号、出現次數、起始偏移量、結束偏移量,出現頻率
檢索過程就是把模糊查詢變成多個可以利用索引的精确查詢的邏輯組合的過程。進而大大提高了多關鍵詞查詢的效率,是以,全文檢索問題歸結到最後是一個排序問題。
由此可以看出,模糊查詢相對資料庫的精确查詢是一個非常不确定的問題,這也是大部分資料庫對全文檢索支援有限的原因。Lucene最核心的特征是通過特殊的索引結構實作了傳統資料庫不擅長的全文索引機制,并提供了擴充接口,以友善針對不同應用的定制。
可以通過一下表格對比一下資料庫的模糊查詢:
Lucene全文索引引擎 | 資料庫 | |
索引 | 将資料源中的資料都通過全文索引一一建立反向索引 | 對于LIKE查詢來說,資料傳統的索引是根本用不上的。資料需要逐個便利記錄進行GREP式的模糊比對,比有索引的搜尋速度要有多個數量級的下降。 |
比對效果 | 通過詞元(term)進行比對,通過語言分析接口的實作,可以實作對中文等非英語的支援。 | 使用:like "%net%" 會把netherlands也比對出來, 多個關鍵詞的模糊比對:使用like "%com%net%":就不能比對詞序颠倒的xxx.net..xxx.com |
比對度 | 有比對度算法,将比對程度(相似度)比較高的結果排在前面。 | 沒有比對程度的控制:比如有記錄中net出現5詞和出現1次的,結果是一樣的。 |
結果輸出 | 通過特别的算法,将最比對度最高的頭100條結果輸出,結果集是緩沖式的小批量讀取的。 | 傳回所有的結果集,在比對條目非常多的時候(比如上萬條)需要大量的記憶體存放這些臨時結果集。 |
可定制性 | 通過不同的語言分析接口實作,可以友善的定制出符合應用需要的索引規則(包括對中文的支援) | 沒有接口或接口複雜,無法定制 |
結論 | 高負載的模糊查詢應用,需要負責的模糊查詢的規則,索引的資料量比較大 | 使用率低,模糊比對規則簡單或者需要模糊查詢的資料量少 |
全文檢索和資料庫應用最大的不同在于:讓最相關的頭100條結果滿足98%以上使用者的需求
2.3. 相對一般的搜尋引擎優點
大部分的搜尋(資料庫)引擎都是用B樹結構來維護索引,索引的更新會導緻大量的IO操作,Lucene在實作中,對此稍微有所改進:不是維護一個索引檔案,而是在擴充索引的時候不斷建立新的索引檔案,然後定期的把這些新的小索引檔案合并到原先的大索引中(針對不同的更新政策,批次的大小可以調整),這樣在不影響檢索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文檢索系統/應用的比較:
Lucene | 其他開源全文檢索系統 | |
增量索引和批量索引 | 可以進行增量的索引(Append),可以對于大量資料進行批量索引,并且接口設計用于優化批量索引和小批量的增量索引。 | 很多系統隻支援批量的索引,有時資料源有一點增加也需要重建索引。 |
資料源 | Lucene沒有定義具體的資料源,而是一個文檔的結構,是以可以非常靈活的适應各種應用(隻要前端有合适的轉換器把資料源轉換成相應結構), | 很多系統隻針對網頁,缺乏其他格式文檔的靈活性。 |
索引内容抓取 | Lucene的文檔是由多個字段組成的,甚至可以控制那些字段需要進行索引,那些字段不需要索引,近一步索引的字段也分為需要分詞和不需要分詞的類型: 需要進行分詞的索引,比如:标題,文章内容字段 不需要進行分詞的索引,比如:作者/日期字段 | 缺乏通用性,往往将文檔整個索引了 |
語言分析 | 通過語言分析器的不同擴充實作: 可以過濾掉不需要的詞:an the of 等, 西文文法分析:将jumps jumped jumper都歸結成jump進行索引/檢索 非英文支援:對亞洲語言,阿拉伯語言的索引支援 | 缺乏通用接口實作 |
查詢分析 | 通過查詢分析接口的實作,可以定制自己的查詢文法規則: 比如: 多個關鍵詞之間的 + - and or關系等 | |
并發通路 | 能夠支援多使用者的使用 |
2.4. 對索引過程優化
索引一般分2種情況,一種是小批量的索引擴充,一種是大批量的索引重建。在索引過程中,并不是每次新的DOC加入進去索引都重新進行一次索引檔案的寫入操作(檔案I/O是一件非常消耗資源的事情)。
Lucene先在記憶體中進行索引操作,并根據一定的批量進行檔案的寫入。這個批次的間隔越大,檔案的寫入次數越少,但占用記憶體會很多。反之占用記憶體少,但檔案IO操作頻繁,索引速度會很慢。在IndexWriter中有一個MERGE_FACTOR參數可以幫助你在構造索引器後根據應用環境的情況充分利用記憶體減少檔案的操作。根據我的使用經驗:預設Indexer是每20條記錄索引後寫入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
2.5. 對搜尋過程優化
lucene支援記憶體索引:這樣的搜尋比基于檔案的I/O有數量級的速度提升。
http://www.onjava.com/lpt/a/3273,而盡可能減少IndexSearcher的建立和對搜尋結果的前台的緩存也是必要的。
Lucene面向全文檢索的優化在于首次索引檢索後,并不把所有的記錄(Document)具體内容讀取出來,而起隻将所有結果中比對度最高的頭100條結果(TopDocs)的ID放到結果集緩存中并傳回,這裡可以比較一下資料庫檢索:如果是一個10,000條的資料庫檢索結果集,資料庫是一定要把所有記錄内容都取得以後再開始傳回給應用結果集的。
是以即使檢索比對總數很多,Lucene的結果集占用的記憶體空間也不會很多。對于一般的模糊檢索應用是用不到這麼多的結果的,頭100條已經可以滿足90%以上的檢索需求。
如果首批緩存結果數用完後還要讀取更後面的結果時Searcher會再次檢索并生成一個上次的搜尋緩存數大1倍的緩存,并再重新向後抓取。是以如果構造一個Searcher去查1-120條結果,Searcher其實是進行了2次搜尋過程:頭100條取完後,緩存結果用完,Searcher重新檢索再構造一個200條的結果緩存,依此類推,400條緩存,800條緩存。由于每次Searcher對象消失後,這些緩存也通路那不到了,你有可能想将結果記錄緩存下來,緩存數盡量保證在100以下以充分利用首次的結果緩存,不讓Lucene浪費多次檢索,而且可以分級進行結果緩存。
Lucene的另外一個特點是在收集結果的過程中将比對度低的結果自動過濾掉了,過濾過程我們可以通過設定最低的比對度來進行過濾。這也是和資料庫應用需要将搜尋的結果全部傳回不同之處。
3. Lucene的特性分析
3.1. Lucene核心部分——索引排序
Lucene 的索引排序是使用了倒排序原理。
該結構及相應的生成算法如下:
設有兩篇文章1和2
文章1的内容為:Tom lives in Guangzhou ,I live in Guangzhou too.
文章2的内容為:He once lived in Shanghai .
1. 由于lucene是基于關鍵詞索引和查詢的,首先我們要取得這兩篇文章的關鍵詞,通常我們需要如下處理措施
a. 我們現在有的是文章内容,即一個字元串,我們先要找出字元串中的所有單詞,即分詞。英文單詞由于用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
b. 文章中的”in”, “once” “too”等詞沒有什麼實際意義,中文中的“的”“是”等字通常也無具體含義, 這些不代表概念的詞可以過濾掉,這個也就是在《Lucene詳細分析》中所講的StopTokens
c. 使用者通常希望查“He”時能把含“he”,“HE”的文章也找出來,是以所有單詞需要統一大小寫。
d. 使用者通常希望查“live”時能把含“lives”,“lived”的文章也找出來,是以需要把“lives”,“lived”還原成“live”
e. 文章中的标點符号通常不表示某種概念,也可以過濾掉,在lucene中以上措施由Analyzer類完成,經過上面處理後:
文章1的所有關鍵詞為:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有關鍵詞為:[he] [live] [shanghai]
2. 有了關鍵詞後,我們就可以建立反向索引了
上面的對應關系是:“文章号”對“文章中所有關鍵詞”。反向索引把這個關系倒過來,變成:“關鍵詞”對“擁有該關鍵詞的所有文章号”。文章1,2經過倒排後變成
關鍵詞 | 文章号 |
guangzhou | 1 |
he | 2 |
i | 1 |
live | 1,2 |
shanghai | 2 |
tom | 1 |
通常僅知道關鍵詞在哪些文章中出現還不夠,我們還需要知道關鍵詞在文章中出現次數和出現的位置,通常有兩種位置:a)字元位置,即記錄該詞是文章中第幾個字元(優點是關鍵詞亮顯時定位快);b)關鍵詞位置,即記錄該詞是文章中第幾個關鍵詞(優點是節約索引空間、詞組(phase)查詢快),lucene中記錄的就是這種位置。
加上“出現頻率”和“出現位置”資訊後,我們的索引結構變為:
關鍵詞 | 文章号[出現頻率] | 出現位置 |
guangzhou | 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 這行為例我們說明一下該結構:live在文章1中出現了2次,文章2中出現了一次,它的出現位置為“2,5, 2” 這表示什麼呢?我們需要結合文章号和出現頻率來分析,文章1中出現了2次,那麼“2,5”就表示live在文章1中出現的兩個位置,文章2中出現了一次,剩下的“2”就表示live是文章2中第 2個關鍵字。
以上就是lucene索引結構中最核心的部分。我們注意到關鍵字是按字元順序排列的(lucene沒有使用B樹結構),是以lucene可以用二進制搜尋算法快速定位關鍵詞。
實作時 lucene将上面三列分别作為詞典檔案(Term Dictionary)、頻率檔案(frequencies)、位置檔案 (positions)儲存。其中詞典檔案不僅儲存有每個關鍵詞,還保留了指向頻率檔案和位置檔案的指針,通過指針可以找到該關鍵字的頻率資訊和位置資訊。
Lucene中使用了field的概念,用于表達資訊所在位置(如标題中,文章中,url中),在建索引中,該field資訊也記錄在詞典檔案中,每個關鍵詞都有一個field資訊(因為每個關鍵字一定屬于一個或多個field)。
為了減小索引檔案的大小,Lucene對索引還使用了壓縮技術。首先,對詞典檔案中的關鍵詞進行了壓縮,關鍵詞壓縮為<字首長度,字尾>,例如:目前詞為“阿拉伯語”,上一個詞為“阿拉伯”,那麼“阿拉伯語”壓縮為<3,語>。其次大量用到的是對數字的壓縮,數字隻儲存與上一個值的內插補點(這樣可以減小數字的長度,進而減少儲存該數字需要的位元組數)。例如目前文章号是16389(不壓縮要用3個位元組儲存),上一文章号是16382,壓縮後儲存7(隻用一個位元組)。
下面我們可以通過對該索引的查詢來解釋一下為什麼要建立索引。
假設要查詢單詞 “live”,lucene先對詞典二進制查找、找到該詞,通過指向頻率檔案的指針讀出所有文章号,然後傳回結果。詞典通常非常小,因而,整個過程的時間是毫秒級的。
而用普通的順序比對算法,不建索引,而是對所有文章的内容進行字元串比對,這個過程将會相當緩慢,當文章數目很大時,時間往往是無法忍受的。
3.2. Lucene的相關度積分公式
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 : 查詢串q中,某個項出項的次數的平方根
tf_d : 文檔d中 ,出現某個項的次數的平方根
numDocs : 在這個索引裡,找到分數大于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中,命中的項數量除以查詢q的項總數
3.3. Lucene的其他特性
3.3.1. Boosting特性
luncene對Document和Field提供了一個可以設定的Boosting參數, 這個參數的用處是告訴lucene, 某些記錄更重要,在搜尋的時候優先考慮他們 比如在搜尋的時候你可能覺得幾個門戶的網頁要比垃圾小站更優先考慮
lucene預設的boosting參數是1.0, 如果你覺得這個field重要,你可以把boosting設定為1.5, 1.2....等, 對Document設定boosting相當設定了它的每個Field的基準boosting,到時候實際Field的boosting就是(Document-boosting*Field-boosting)設定了一遍相同的boosting.
似乎在lucene的記分公式裡面有boosting參數,不過我估計一般人是不會去研究他的公式的(複雜),而且公式也無法給出最佳值,是以我們所能做的隻能是一點一點的改變boosting, 然後在實際檢測中觀察它對搜尋結果起到多大的作用來調整
一般的情況下是沒有必要使用boosting的, 因為搞不好你就把搜尋給搞亂了, 另外如果是單獨對Field來做Bossting, 也可以通過将這個Field提前來起到近似的效果
3.3.2. Indexing Date
日期是lucene需要特殊考慮的地方之一, 因為我們可能需要對日期進行範圍搜尋, Field.keyword(string,Date)提供了這樣的方法,lucene會把這個日期轉換為string, 值得注意的是這裡的日期是精确到毫秒的,可能會有不必要的性能損失, 是以我們也可以把日期自行轉化為YYYYMMDD這樣的形勢,就不用精确到具體時間了,通過File.keyword(Stirng,String) 來index, 使用PrefixQuery 的YYYY一樣能起到簡化版的日期範圍搜尋(小技巧), lucene提到他不能處理1970年以前的時間,似乎是上一代電腦系統遺留下來的毛病
3.3.3. Indexing 數字
如果數字隻是簡單的資料, 比如中國有56個民族. 那麼可以簡單的把它當字元處理
如果數字還包含數值的意義,比如價格, 我們會有範圍搜尋的需要(20元到30元之間的商品),那麼我們必須做點小技巧, 比如把3,34,100 這三個數字轉化為003,034,100 ,因為這樣處理以後, 按照字元排序和按照數值排序是一樣的,而lucene内部按照字元排序,003->034->100 NOT(100->3->34)
3.3.4. 排序
Lucene預設按照相關度(score)排序,為了能支援其他的排序方式,比如日期,我們在add Field的時候,必須保證field被Index且不能被tokenized(分詞),并且排序的隻能是數字,日期,字元三種類型之一
3.3.5. Lucene的IndexWriter調整
IndexWriter提供了一些參數可供設定,清單如下
屬性 | 預設值 | 說明 | |
mergeFactor | org.apache.lucene.mergeFactor | 10 | 控制index的大小和頻率,兩個作用 |
maxMergeDocs | org.apache.lucene.maxMergeDocs | Integer.MAX_VALUE | 限制一個段中的document數目 |
minMergeDocs | org.apache.lucene.minMergeDocs | 10 | 緩存在記憶體中的document數目,超過他以後會寫入到磁盤 |
maxFieldLength | 1000 | 一個Field中最大Term數目,超過部分忽略,不會index到field中,是以自然也就搜尋不到 |
這些參數的的詳細說明比較複雜:mergeFactor有雙重作用
設定每mergeFactor個document寫入一個段,比如每10個document寫入一個段
設定每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
3.3.6. RAMDirectory 和 FSDirectory 轉化
RAMDirectory(RAMD)在效率上比FSDirectyr(FSD)高不少, 是以我們可以手動的把RAMD當作FSD的buffer,這樣就不用去很費勁的調優FSD那麼多參數了,完全可以先用RAM跑好了index, 周期性(或者是别的什麼算法)來回寫道FSD中。 RAMD完全可以做FSD的buffer。
3.3.7. 為查詢優化索引(index)
Indexwriter.optimize()方法可以為查詢優化索引(index),之前提到的參數調優是為indexing過程本身優化,而這裡是為查詢優化,優化主要是減少index檔案數,這樣讓查詢的時候少打開檔案,優化過程中,lucene會拷貝舊的index再合并,合并完成以後删除舊的index,是以在此期間,磁盤占用增加, IO符合也會增加,在優化完成瞬間,磁盤占用會是優化前的2倍,在optimize過程中可以同時作search。
3.3.8. 并發操作Lucene和locking機制
v 所有隻讀操作都可以并發
v 在index被修改期間,所有隻讀操作都可以并發
v 對index修改操作不能并發,一個index隻能被一個線程占用
v index的優化,合并,添加都是修改操作
v IndexWriter和IndexReader的執行個體可以被多線程共享,他們内部是實作了同步,是以外面使用不需要同步
3.3.9. Locing
lucence内部使用檔案來locking, 預設的locking檔案放在java.io.tmpdir,可以通過-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock兩個檔案,lock檔案用來防止并行操作index,如果并行操作, lucene會抛出異常,可以通過設定-DdisableLuceneLocks=true來禁止locking,這樣做一般來說很危險,除非你有作業系統或者實體級别的隻讀保證,比如把index檔案刻盤到CDROM上。
4. Lucene文檔結構
Lucene中最基礎的概念是索引(index),文檔(document.,域(field)和項(term)。
索引包含了一個文檔的序列。
· 文檔是一些域的序列。
· 域是一些項的序列。
· 項就是一個字串。
存在于不同域中的同一個字串被認為是不同的項。是以項實際是用一對字串表示的,第一個字串是域名,第二個是域中的字串。
4.1. Lucene概念詳細介紹
4.1.1. 域的類型
Lucene中,域的文本可能以逐字的非倒排的方式存儲在索引中。而倒排過的域稱為被索引過了。域也可能同時被存儲和被索引。
域的文本可能被分解許多項目而被索引,或者就被用作一個項目而被索引。大多數的域是被分解過的,但是有些時候某些辨別符域被當做一個項目索引是很有用的。
4.1.2. 段(Segment)
Lucene索引可能由多個子索引組成,這些子索引成為段。每一段都是完整獨立的索引,能被搜尋。索引是這樣作成的:
1. 為新加入的文檔建立新段。
2. 合并已經存在的段。
搜尋時需要涉及到多個段和/或者多個索引,每一個索引又可能由一些段組成。
4.1.3. 文檔号(document.nbspNumber)
内部的來說,Lucene用一個整形(interger)的文檔号來訓示文檔。第一個被加入到索引中的文檔就是0号,順序加入的文檔将得到一個由前一個号碼遞增而來的号碼。
注意文檔号是可能改變的,是以在Lucene外部存儲這些号碼時必須小心。特别的,号碼的改變的情況如下:
· 隻 有段内的号碼是相同的,不同段之間不同,因而在一個比段廣泛的上下文環境中使用這些号碼時,就必須改變它們。标準的技術是根據每一段号碼多少為每一段配置設定 一個段号。将段内文檔号轉換到段外時,加上段号。将某段外的文檔号轉換到段内時,根據每段中可能的轉換後号碼範圍來判斷文檔屬于那一段,并減調這一段的段 号。例如有兩個含5個文檔的段合并,那麼第一段的段号就是0,第二段段号5。第二段中的第三個文檔,在段外的号碼就是8。
· 文檔删除後,連續的号碼就出現了間斷。這可以通過合并索引來解決,段合并時删除的文檔相應也删掉了,新合并而成的段并沒有号碼間斷。
4.1.4. 索引資訊
索引段維護着以下的資訊:
· 域集合。包含了索引中用到的所有的域。
· 域值存儲表。每一個文檔都含有一個“屬性-值”對的清單,屬性即為域名。這個清單用來存儲文檔的一些附加資訊,如标題,url或者通路資料庫的一個ID。在搜尋時存儲域的集合可以被傳回。這個表以文檔号辨別。
· 項字典。這個字典含有所有文檔的所有域中使用過的的項,同時含有使用過它的文檔的文檔号,以及指向使用頻數資訊和位置資訊的指針。
· 項頻數資訊。對于項字典中的每個項,這些資訊包含含有這個項的文檔的總數,以及每個文檔中使用的次數。
· 項位置資訊。對于項字典中的每個項,都存有在每個文檔中出現的各個位置。
· 标準化因子。對于文檔中的每一個域,存有一個值,用來以後乘以這個這個域的命中數(hits)。
· 被删除的文檔資訊。這是一個可選檔案,用來表明那些文檔已經删除了。
接下來的各部分部分較長的描述這些資訊。
4.1.5. 檔案的命名(File Naming)
同屬于一個段的檔案擁有相同的檔案名,不同的擴充名。擴充名由以下讨論的各種檔案格式确定。
一般來說,一個索引存放一個目錄,其所有段都存放在這個目錄裡,不這樣作,也是可以的,在性能方面較低。
4.2. Lucene基本資料類型(Primitive Types)
4.2.1. 位元組Byte
最基本的資料類型就是位元組(byte,8位)。檔案就是按位元組順序通路的。其它的一些資料類型也定義為位元組的序列,檔案的格式具有位元組意義上的獨立性。
UInt32 :32位無符号整數,由四個位元組組成,高位優先。UInt32 --> <Byte>4
Uint64 : 64位無符号整數,由八位元組組成,高位優先。UInt64 --> <Byte>8
VInt : 可變長的正整數類型,每位元組的最高位表明還剩多少位元組。每位元組的低七位表明整數的值。是以單位元組的值從0到127,兩位元組值從128到16,383,等等。
VInt 編碼示例
value
First byte
Second byte
Third byte
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
... 這種編碼提供了一種在高效率解碼時壓縮資料的方法。
4.2.2. 字元串Chars
Lucene輸出UNICODE字元序列,使用标準UTF-8編碼。
String :Lucene輸出由VINT和字元串組成的字串,VINT表示字串長,字元串緊接其後。
String --> VInt, Chars
4.3. 索引包含的檔案(Per-Index Files)
4.3.1. Segments檔案
索引中活動的段存儲在Segments檔案中。每個索引隻能含有一個這樣的檔案,名為"segments".這個檔案依次列出每個段的名字和每個段的大小。
Segments --> SegCount, <SegName, SegSize>SegCount
SegCount, SegSize --> UInt32
SegName --> String
SegName表示該segment的名字,同時作為索引其他檔案的字首。
SegSize是段索引中含有的文檔數。
4.3.2. Lock檔案
有一些檔案用來表示另一個程序在使用索引。
· 如果存在"commit.lock"檔案,表示有程序在寫"segments"檔案和删除無用的段索引檔案,或者表示有程序在讀"segments"檔案和打開某些段的檔案。在一個程序在讀取"segments"檔案段資訊後,還沒來得及打開所有該段的檔案前,這個Lock檔案可以防止另一個程序删除這些檔案。
· 如果存在"index.lock"檔案,表示有程序在向索引中加入文檔,或者是從索引中删除文檔。這個檔案防止很多檔案同時修改一個索引。
4.3.3. Deleteable檔案
名為"deletetable"的檔案包含了索引不再使用的檔案的名字,這些檔案可能并沒有被實際的删除。這種情況隻存在與Win32平台下,因為Win32下檔案仍打開時并不能删除。
Deleteable --> DelableCount, <DelableName>DelableCount
DelableCount --> UInt32
DelableName --> String
4.3.4. 段包含的檔案(Per-Segment Files)
剩下的檔案是每段中包含的檔案,是以由字尾來區分。
域(Field)
域集合資訊(Field Info)
所有域名都存儲在這個檔案的域集合資訊中,這個檔案以字尾.fnm結尾。
FieldInfos (.fnm) --> FieldsCount, <FieldName, FieldBits>FieldsCount
FieldsCount --> VInt
FieldName --> String
FieldBits --> Byte
目前情況下,FieldBits隻有使用低位,對于已索引的域值為1,對未索引的域值為0。
檔案中的域根據它們的次序編号。是以域0是檔案中的第一個域,域1是接下來的,等等。這個和文檔号的編号方式相同。
4.3.5. 域值存儲表(Stored Fields)
域值存儲表使用兩個檔案表示:
1. 域索引(.fdx檔案)。
如下,對于每個文檔這個檔案包含指向域值的指針:
FieldIndex (.fdx) --> <FieldvaluesPosition>SegSize
FieldvaluesPosition --> Uint64
FieldvaluesPosition訓示的是某一文檔的某域的域值在域值檔案中的位置。因為域值檔案含有定長的資料資訊,因而很容易随機通路。在域值檔案中,文檔n的域值資訊就存在n*8位置處(The position of document.nbspn's field data is the Uint64 at n*8 in this file.)。
2. 域值(.fdt檔案)。
如下,每個文檔的域值資訊包含:
FieldData (.fdt) --> <DocFieldData>SegSize
DocFieldData --> FieldCount, <FieldNum, Bits, value>FieldCount
FieldCount --> VInt
FieldNum --> VInt
Bits --> Byte
value --> String
目前情況下,Bits隻有低位被使用,值為1表示域名被分解過,值為0表示未分解過。÷
4.3.6. 項字典(Term Dictionary)
項字典用以下兩個檔案表示:
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
項資訊按項排序。項資訊排序時先按項所屬的域的文字順序排序,然後按照項的字串的文字順序排序。
項的字字首往往是共同的,與字的字尾組成字。PrefixLength變量就是表示與前一項相同的字首的字數。是以,如果前一個項的字是"bone",後一個是"boy"的話,PrefixLength值為2,Suffix值為"y"。
FieldNum指明了項屬于的域号,而域名存儲在.fdt檔案中。
DocFreg表示的是含有該項的文檔的數量。
FreqDelta指明了項所屬TermFreq變量在.frq檔案中的位置。詳細的說,就是指相對于前一個項的資料的位置偏移量(或者是0,表示檔案中第一個項)。
ProxDelta指明了項所屬的TermPosition變量在.prx檔案中的位置。詳細的說,就是指相對于前一個項的資料的位置偏移量(或者是0,表示檔案中第一個項)。
2. 項資訊索引(.tii檔案)。
每個項資訊索引檔案包含.tis檔案中的128個條目,依照條目在.tis檔案中的順序。這樣設計是為了一次将索引資訊讀入記憶體能,然後使用它來随機的通路.tis檔案。
這個檔案的結構和.tis檔案非常類似,隻在每個條目記錄上增加了一個變量IndexDelta。
TermInfoIndex (.tii)--> IndexTermCount, TermIndices
IndexTermCount --> UInt32
TermIndices --> <TermInfo, IndexDelta>IndexTermCount
IndexDelta --> VInt
IndexDelta表示該項的TermInfo變量值在.tis檔案中的位置。詳細的講,就是指相對于前一個條目的偏移量(或者是0,對于檔案中第一個項)。
4.3.7. 項頻數(Frequencies)
.frq檔案包含每一項的文檔的清單,還有該項在對應文檔中出現的頻數。
FreqFile (.frq) --> <TermFreqs>TermCount
TermFreqs --> <TermFreq>DocFreq
TermFreq --> DocDelta, Freq?
DocDelta,Freq --> VInt
TermFreqs序列按照項來排序(依據于.tis檔案中的項,即項是隐含存在的)。
TermFreq元組按照文檔号升序排列。
DocDelta決定了文檔号和頻數。詳細的說,DocDelta/2表示相對于前一文檔号的偏移量(或者是0,表示這是TermFreqs裡面的第一項)。當DocDelta是奇數時表示在該文檔中頻數為1,當DocDelta是偶數時,另一個VInt(Freq)就表示在該文檔中出現的頻數。
例如,假設某一項在文檔7中出現一次,在文檔11中出現了3次,在TermFreqs中就存在如下的VInts序列:
15, 22, 3
4.3.8. 項位置(Position)
.prx檔案包含了某文檔中某項出現的位置資訊的清單。
ProxFile (.prx) --> <TermPositions>TermCount
TermPositions --> <Positions>DocFreq
Positions --> <PositionDelta>Freq
PositionDelta --> VInt
TermPositions按照項來排序(依據于.tis檔案中的項,即項是隐含存在的)。
Positions元組按照文檔号升序排列。
PositionDelta是相對于前一個出現位置的偏移位置(或者為0,表示這是第一次在這個文檔中出現)。
例如,假設某一項在某文檔第4項出現,在另一個文檔中第5項和第9項出現,将存在如下的VInt序列:
4, 5, 4
4.3.9. 标準化因子(Normalization Factor)
.nrm檔案包含了每個文檔的标準化因子,标準化因子用來以後乘以這個這個域的命中數。
Norms (.nrm) --> <Byte>SegSize
每個位元組記錄一個浮點數。位0-2包含了3位的尾數部分,位3-8包含了5位的指數部分。
按如下規則可将這些位元組轉換為IEEE标準單精度浮點數:
1. 如果該位元組是0,就是浮點0;
2. 否則,設定新浮點數的标志位為0;
3. 将位元組中的指數加上48後作為新的浮點數的指數;
4. 将位元組中的尾數映射到新浮點數尾數的高3位;并且
5. 設定新浮點數尾數的低21位為0。
4.3.10. 被删除的文檔(Deleted document)
.del檔案是可選的,隻有在某段中存在删除操作後才存在:
Deletions (.del) --> ByteCount,BitCount,Bits
ByteSize,BitCount --> Uint32
Bits --> <Byte>ByteCount
ByteCount表示的是Bits清單中Byte的數量。典型的,它等于(SegSize/8)+1。
BitCount表示Bits清單中多少個已經被設定過了。
Bits清單包含了一些位(bit),順序表示一個文檔。當對應于文檔号的位被設定了,就标志着這個文檔已經被删除了。位的順序是從低到高。是以,如果Bits包含兩個位元組,0x00和0x02,那麼表示文檔9已經删除了。
4.3.11. 局限性(Limitations)
在以上的檔案格式中,好幾處都有限制項和文檔的最大個數為32位數的極限,即接近于40億。今天看來,這不會造成問題,但是,長遠的看,可能造成問題。是以,這些極限應該或者換為UInt64類型的值,或者更好的,換為VInt類型的值(VInt值沒有上限)。
有兩處地方的代碼要求必須是定長的值,他們是:
1. FieldvaluesPosition變量(存儲于域索引檔案中,.fdx檔案)。它已經是一個UInt64型,是以不會有問題。
2. TermCount變量(存儲于項資訊檔案中,.tis檔案)。這是最後輸出到檔案中的,但是最先被讀取,是以是存儲于檔案的最前端 。索引代碼先在這裡寫入一個0值,然後在其他檔案輸出完畢後覆寫這個值。是以無論它存儲在什麼地方,它都必須是一個定長的值,它應該被變成UInt64型。
除此之外,所有的UInt值都可以換成VInt型以去掉限制。
5. Lucene代碼分析
應用情景分析 | ||
Query query = parser.parse(queries[j]); | 獲得布爾查詢 | |
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) ² IndexSearcher 開始時已經打開了該目錄 ² IndexSearcher 中初始化了IndexReader ² IndexReader中讀取了SegmentInfos ² IndexReader = SegmentReader ² SegmentReader ::initialize(SegmentInfo si) n 1。讀入域資訊,隻有域的名字 n 2. 打開儲存域、儲存域索引的檔案 | ||
Scorer scorer = query.weight(this).scorer(reader) u 這裡query = PhraseQuery u query.weight(this) 獲得PhraseWeight(IndexSearcher) u PhraseWeight::scorer(IndexReader reader) u PhraseQuery::TermPositions p = reader.termPositions((Term)terms.elementAt(i)); u public TermPositions termPositions(Term term) throws IOException { IndexReader::TermPositions termPositions = termPositions();
IndexReader = SegmentReader, IndexSearcher termPositions.seek(term);
return termPositions; ² SegmentReader. termPositions()::return SegmentTermPositions(this)` | ||
<p>一個權重由query建立,并給查詢器({@link Query#createWeight(Searcher)})使用,方法 {@link #sumOfSquaredWeights()},然後被最進階的查詢api調用 用來計算查詢規範化因子 (@link Similarity#queryNorm(float)}),然後該因子傳給{@link #normalize(float)} 然後被{@link #scorer(IndexReader)}調用 | ||
應用情景分析
6. 測試的主程式
規則: 加粗體的黑色代碼,表示将作深入分析 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); 以上代碼,初始化查詢,分析編号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); 以上代碼,初始化查詢,分析編号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()); } |
查詢結果: Query: "a c e" 3 total results 0 1.0 a c e a c e 1 0.9428091 a c e 2 0.7071068 a c e a b c |
1.1. Searcher searcher = new IndexSearcher(directory)
1.1.1. 初始化
通過目錄,建立一個索引搜尋器,
調用類
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();
}
}
代碼到這裡,已經讀取了檔案segments檔案,獲得段資訊,該測試隻有一個段,是以執行了return new SegmentReader(infos, infos.info(0), closeDirectory);,記住IndexReader = SegmentReader
infos.read(directory):
,該段代碼比較簡單,讀者可以從看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);
private void initialize(SegmentInfo si) throws IOException
{
segment = si.name;
// Use compound file directory for some files, if it exists
Directory cfsDir = directory();// 就是儲存該段的目錄
// CompoundFileReader(組合檔案讀取器)也是(目錄)的子類
if (directory().fileExists(segment + ".cfs")) {
cfsReader = new CompoundFileReader(directory(), segment + ".cfs");
cfsDir = cfsReader;
}
// 1。讀入域資訊,隻有域的名字
fieldInfos = new FieldInfos(cfsDir, segment + ".fnm"); // 這個過程讀入所有的域資訊了
// 2。打開儲存域、儲存域索引的檔案
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);
}
}
1.2. hits = searcher.search(query);
這時,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 = 0.0f ;
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(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);
}
1.2.1. Scorer scorer = query.weight(this).scorer(reader);
參數分析:query = PhraseQuery (該參數由主測試程式中的Query query = parser.parse(queries[j]);初始化)
this = IndexSearcher(該參數初始化,已經初始化了主要的檔案,具體可參考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) 建立了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)); } |
ü TermPositions p = reader.termPositions((Term)terms.elementAt(i));
這時Term文本為查詢裡的項
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 {
根據項,從項資訊讀取器中讀取對應的項資訊,該方法是線程安全的
TermInfo ti = parent.tis.get(term);
seek(ti);
}
seek(TermInfo ti)
SegmentTermDocs的項資訊轉變為現在讀入的項的資訊
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);
調用超類構造器,獲得短語位置的頻繁度資訊和位置資訊,并構造一個優先隊列
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 放在一個一般的隊列裡面(以連結清單形式)
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
}
把各個位置的文檔和得分收集
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(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
}
}
}
到這裡就出來了查詢的文檔和分數,并且這些文檔和分數經過了指定的排序和過濾