由于前一章所述的Lucene的事務性,使得Lucene可以增量的添加一個段,我們知道,倒排索引是有一定的格式的,而這個格式一旦寫入是非常難以改變
的,那么如何能夠增量建索引呢?Lucene使用段這個概念解決了這個問題,對于每個已經生成的段,其倒排索引結構不會再改變,而增量添加的文檔添加到新
的段中,段之間在一定的時刻進行合并,從而形成新的倒排索引結構。
然而也正因為Lucene的事務性,使得Lucene的索引不夠實時,如果想Lucene實時,則必須新添加的文檔后IndexWriter需要
commit,在搜索的時候IndexReader需要重新的打開,然而當索引在硬盤上的時候,尤其是索引非常大的時候,IndexWriter的
commit操作和IndexReader的open操作都是非常慢的,根本達不到實時性的需要。
好在Lucene提供了RAMDirectory,也即內存中的索引,能夠很快的commit和open,然而又存在如果索引很大,內存中不能夠放下的問題。
所以要構建實時的索引,就需要內存中的索引RAMDirectory和硬盤上的索引FSDirectory相互配合來解決問題。
1、初始化階段
首先假設我們硬盤上已經有一個索引FileSystemIndex,由于IndexReader打開此索引非常的慢,因而其是需要事先打開的,并且不會時常的重新打開。
我們在內存中有一個索引MemoryIndex,新來的文檔全部索引到內存索引中,并且是索引完IndexWriter就commit,IndexReader就重新打開,這兩個操作時非常快的。
如下圖,則此時新索引的文檔全部能被用戶看到,達到實時的目的。
2、合并索引階段
然而經過一段時間,內存中的索引會比較大了,如果不合并到硬盤上,則可能造成內存不夠用,則需要進行合并的過程。
當然在合并的過程中,我們依然想讓我們的搜索是實時的,這是就需要一個過渡的索引,我們稱為MergingIndex。
一旦內存索引達到一定的程度,則我們重新建立一個空的內存索引,用于合并階段索引新的文檔,然后將原來的內存索引稱為合并中索引,并啟動一個后臺線程進行合并的操作。
在合并的過程中,如果有查詢過來,則需要三個IndexReader,一個是內存索引的IndexReader打開,這個過程是很快的,一個是合并中索引
的
IndexReader打開,這個過程也是很快的,一個是已經打開的硬盤索引的IndexReader,無需重新打開。這三個IndexReader可以
覆蓋所有的文檔,唯一有可能重復的是,硬盤索引中已經有一些從合并中索引合并過去的文檔了,然而不用擔心,根據Lucene的事務性,在硬盤索引的
IndexReader沒有重新打開的情況下,背后的合并操作它是看不到的,因而這三個IndexReader所看到的文檔應該是既不少也不多。合并使用
IndexWriter(硬盤索引).addIndexes(IndexReader(合并中索引)),合并結束后Commit。
如下圖:
查看原圖(大圖)
3、重新打開硬盤索引的IndexReader
當合并結束后,是應該重新打開硬盤索引的時候了,然而這是一個可能比較慢的過程,在此過程中,我們仍然想保持實時性,因而在此過程中,合并中的索引不能丟
棄,硬盤索引的IndexReader也不要動,而是為硬盤索引打開一個臨時的IndexReader,在打開的過程中,如果有搜索進來,返回的仍然是上
述的三個IndexReader,仍能夠不多不少的看到所有的文檔,而將要打開的臨時的IndexReader將能看到合并中索引和原來的硬盤索引所有的
文檔,此IndexReader并不返回給客戶。如下圖:
查看原圖(大圖)
4、替代IndexReader
當臨時的IndexReader被打開的時候,其看到的是合并中索引的IndexReader和硬盤索引原來的IndexReader之和,下面要做的是:
(1) 關閉合并中索引的IndexReader
(2) 拋棄合并中索引
(3) 用臨時的IndexReader替換硬盤索引原來的IndexReader
(4) 關閉硬盤索引原來的IndexReader。
上面說的這幾個操作必須是原子性的,如果做了(2)但沒有做(3),如果來一個搜索,則將少看到一部分數據,如果做了(3)沒有做(2)則,多看到一部分數據。
所以在進行上述四步操作的時候,需要加一個鎖,如果這個時候有搜索進來的時候,或者在完全沒有做的時候得到所有的IndexReader,或者在完全做好
的時候得到所有的IndexReader,這時此搜索可能被block,但是沒有關系,這四步是非常快的,絲毫不影響替代性。
如下圖:
查看原圖(大圖)
經過這幾個過程,又達到了第一步的狀態,則進行下一個合并的過程。
5、多個索引
有一點需要注意的是,在上述的合并過程中,新添加的文檔是始終添加到內存索引中的,如果存在如下的情況,索引速度實在太快,在合并過程沒有完成的時候,內
存索引又滿了,或者硬盤上的索引實在太大,合并和重新打開要花費太長的時間,使得內存索引以及滿的情況下,還沒有合并完成。
為了處理這種情況,我們可以擁有多個合并中的索引,多個硬盤上的索引,如下圖:
查看原圖(大圖)
新添加的文檔永遠是進入內存索引
當內存索引到達一定的大小的時候,將其加入合并中索引鏈表
有一個后臺線程,每隔一定的時刻,將合并中索引寫入一個新的硬盤索引中取。這樣可以避免由于硬盤索引過大而合并較慢的情況。硬盤索引的
IndexReader也是寫完并重新打開后才替換合并中索引的IndexReader,新的硬盤索引也可保證打開的過程不會花費太長時間。
這樣會造成硬盤索引很多,所以,每隔一定的時刻,將硬盤索引合并成一個大的索引。也是合并完成后方才替換IndexReader
大家可能會發現,此合并的過程和Lucene的段的合并很相似。然而Lucene的一個函數IndexReader.reopen一直是沒有實現的,也即
我們不能選擇哪個段是在內存中的,可以被打開,哪些是硬盤中的,需要在后臺打開然后進行替換,而IndexReader.open是會打開所有的內存中的
和硬盤上的索引,因而會很慢,從而降低了實時性。