Lucene主要有兩種文檔模型:Document和Field,一個Document可能包含若干個Field。
每一個Field有不同的策略:
1.被索引 or not,將該字段(Field)經過分析(Analyisi)後,加入索引中,並不是原文 。
2.如果被索引,可選擇是否保存“term vector”(向量),用於相似檢索。
3.可選擇是否存儲(store),將原文直接拷貝 ,不做索引,用於檢索後的取出。
Lucene中的文檔模型類似於數據庫,但是又不完全相同,體現在如下幾方面:
1.無規範格式,即無需固定的Schema,無列等預先設計,同一個索引中加入的Document可包含不同的Field 。
2.非正規化,Lucene中的文檔模型是一個平面化 的結構,沒有遞歸定義,自然連接等等複雜的結構。
2.2 理解索引過程
總體來說,索引過程爲:
1.提取摘要:從原文提取,並創建Document和Field對象。Tika 提供了PDF、Word等非文本的文本提取。
2.分析:Analysis,首先對Document的Field進行分解,產生token流,然後經過一系列Filter(如小寫化)等。
3.建立索引:通過IndexWriter的addDocument寫入到索引中。Lunece使用了反向索引 ,即“那個Document包含單詞X”,而不是“Document包含哪些Word”
索引文件組成
爲了保證效率,每個索引由若干segments組成:
_X.cfs 每個segments由若干個cfs組成,X爲0,1,2….如果開啓了useCompoundFile,則只有一個.cfs文件。
segments_<N>:記載每個分區對應的cfs文件。
每個一段時間後,在調用IndexWriter時,會自動合併這些segment
2.3 索引的基本操作
首先創建IndexWriter
IndexWriter(dir,new WhiteSpaceAnalyser(),IndexWriter.MaxField.UNLIMITED);
dir是索引的保存路徑,WhiteSpaceAnalyser是基於空白的分詞 ,最後部限定Field的數量。
依次創建文檔Document和Field
Document doc = new Document();
doc.add(new Filed(key,value,STORE?,INDEX?)
key就是field的檢索字段名,value就是待寫入/分析的文本。
STORE ,與索引無關,是否額外存儲原文 ,可以在搜索結果後調用出來,NO不額外存儲;YES,額外存儲。
INDEX ,NO,不索引;ANALYZED,分詞後索引;NOT_ANALYZED,不分詞索引;ANALYZED_NO_NORMS,分詞索引,不存儲NORMS;NOT_ANALYZED_NO_NORMS,不分詞,索引,不存儲NORMS。除了NO外都算索引,可以搜索 。NORMS存儲了boost所需信息,包含了NORM可能會佔用更多內存?
刪除索引
IndexWriter提供了刪除Document的功能:
deleteDocumen(Term)
deleteDocumen(Term[])
deleteDocumen(Query)
deleteDocumen(Query [])
特別注意Term不一定是唯一的,所以有可能誤刪除多個 。另外最好選擇唯一的、非索引的Term 以防混亂(比如唯一ID)。
刪除後commit()然後close才能真正寫入索引文件中。
刪除後只是標記爲刪除,maxDoc()返回所有文檔(含已經刪除,但未清理的);numDocs:未刪除的文檔數量
使用delete後,再optimize():壓縮刪除的空間、再commit才真正的刪除釋放空間。
更新索引
updateDocument(Term,Document),Lunce只支持全部替換,即整個Docuemnt要被替換掉,沒法更新單獨的Field。
2.4 Field的選項
選項分爲三類:index、storing和term vector。
Index選項
Index.ANALYZED :分詞後索引
Index.NOT_ANALYZED : 不分詞直接索引,例如URL、系統路徑等,用於精確檢索 。
Index.ANALYZED_NO_NORMS : 類似Index.ANALYZED,但不存儲NORM TERMS,節約內存但不支持Boost。
Index.NOT_ANALYZED_NO_NORMS : 類似Index.NOT_ANALYZED,但不存儲NORM TERMS,節約內存但不支持Boost,非常常用 。
Index.NO : 根本不索引,所以不會被檢索到
默認情況,Luncene會存儲所有單詞的出現位置,可以用Field.setOmitTermFreqAndPositions(true)關閉,但是會影響PhraseQuery和SpanQuery。
Store選項
Store.YES :存儲原始value數值,可在檢索後被提取 。
Store.NO :不存儲原始數值,檢索後無法重新提取。
CompressionTools 可用於壓縮、解壓縮byte數組。
Term Vector選項
Term Vector主要用於爲相似搜索 提供支持 ,例如搜索cat,返回cat。
TermVector.YES :記錄Term Vector
TermVector.WITH_POSITIONS :記錄Term Vector以及每個Term出現的位置
TermVector.WITH_OFFSETS :記錄Term Vector以及每個Term出現的偏移
TermVector.WITH_POSITIONS_OFFSETS :記錄Term Vector以及出現的位置+偏移
TermVector.NO :不存儲TermVector
如果Index選擇了No,則TermVector必須選擇No
將String外的類型作爲Field的數據源
Reader:無法被STORE,默認TokenStream始終被分詞和索引。
TokenStream:分詞之後的結果作爲源,無法被Store,始終analyzed並索引。
byte[] :無法被索引,沒有TermVector,必須被Store.YES
與排序相關選項
數字Field可以用NumericField,如果是文本Field必須Field.Index.NOT_ANALYZED,才能排序,即保證這個Field只含有一個Token才能排序 。
多值Field(Multi-valued Fields)
比如一本書有多個作者,怎麼辦呢?
一種方法是,添加多個同一key,不同value的Field
Document doc = new Document();
for (int i = 0; i < authors.length; i++) {
doc.add(new Field(“author”, authors[i],
Field.Store.YES,
Field.Index.ANALYZED));
}
還有一種方法在第4章中提出。
2.5 Boost(提升)
boost可以對影響搜索返回結果的排序 。
boost可以在index或者搜索時候完成,後者更具有靈活性可獨立制定但耗費更多CPU。
Booost Doument
index時候boost將存儲在NORMS TERM中。默認情況下,所有Document有相等的Boost,即1.0,可以手動提升一個Docuemnt的Boost數值。
Document.settBoost(float bei),bei是1.0的倍數。
Boost Field
也可以對Field進行索引,使用Document的Boost,對下屬的Field都執行相同的Field。
單獨對Field進行Boost
Field.boost(float)
注意:Lucene的Rank算法由多種因素組成,Boost只是一個因素之一,不是決定性因素 。
Norms
boost的數值存儲在Norms中,可能會導致Search時佔用大量內存。因此可將其關閉:
設置NO_NORMS,或者再Field中指定Field.setOmitNorms(true)。
2.6 對數字、日期、時間等進行索引
索引數字
有兩種場景:
1.數字嵌入在Text中,例如“Be sure to include Form 1099 in your tax return”,而你想要搜索1099這個詞。此時需要選擇不分解數字的Analyzer ,例如WhitespaceAnalyzer或者StandardAnalyzer。而SimpleAnalyzer和StopAnalyzer會忽略數字,無法通過1099檢出。
2.數字式單獨的Field,2.9之後,Lucene支持了數字類型,使用NumericField即可:doc.add(new NumericField(“price”).setDoubleValue(19.99));此時,對數字Field使用字典樹存儲,
可向document中添加一樣的NumericField數值,在NumericRangeQuery、NumericRangeFilter中以or的方式支持,但是排序中不支持。因此如果要排序,必須添加唯一的NumericField。
precisionStep控制了掃描精度,越小越精確但速度越慢。
索引日期和時間
方法是:將日期轉化爲時間戳(長整數) ,然後按照NumericField進行處理。
或者,如果不需要精確到毫秒,可以轉化成秒處理
doc.add(new NumericField(“day”) .setIntValue((int) (new Date().getTime()/24/3600)));
甚至對某一天進行索引而不是具體時間。
Calendar cal = Calendar.getInstance();
cal.setTime(date);
doc.add(new NumericField(“dayOfMonth”)
.setIntValue(cal.get(Calendar.DAY_OF_MONTH)));
2.7 Field截斷
Lucene支持對字段的截斷。IndexWriter.MaxFieldLength表示字段的最大長度,默認爲MaxFieldLength.UNLIMITED,無限。
而MaxFieldLength.LIMITED表示有限制,可以通過setMaxFieldLength(int n)進行指定。
上述設定之後,只保留前n個字符。
可以通過setInfoStream(System.out)獲得詳細日誌信息。
2.8 實時搜索
2.9後支持實時搜索,或者說很快的入索引–檢索過程 。
IndexReader IndexWriter.getReader()
本方法將立即刷新Index的緩存,生效後立即返回IndexReader用於搜索。
2.9 優化索引
索引優化可以提升搜索速度 ,而非索引速度。它指的是將小索引文件合併成幾個。
IndexWriter提供了幾個優化方法:
optimize():將索引合併爲一個段,完成前不會返回。但是太耗費資源。
optimize(int maxNumSegments):部分優化,優化到最多maxNumSegments個段?是優化於上述極端情況的這種,例如5個。
optimize(boolean doWait):通optimize(),但是它將立即返回。
optimize(int maxNumSegments, boolean doWait):同optimize(int maxNumSegments),但是將立即返回。
另外:在優化中會耗費大量的額外空間 。即舊的廢棄段直到IndexWriter.commit()之後才能被移除 。
2.10 Directory
Directory封裝了存儲的API,向上提供了抽象的接口,有以下幾類:
SimpleFSDirectory:存儲於本地磁盤使用java.io,不支持多線程,要自己加鎖 。
NIOFSDirectory:多線程可拓展,使用java.nio,支持多線程安全,但是Windows下有Bug 。
MMapDirectory:內存映射存儲(將文件映射到內存中進行操作,類似nmap)。
RAMDirectory:全部在內存中存儲。
FileSwitchDirectory:使用兩個目錄,切換交替使用。
使用FSDirectory.open將自動挑選合適的Directory。也可以自己指定:
Directory ramDir = new RAMDirectory();
IndexWriter writer = new IndexWriter(ramDir, analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
RAMDirectory適用於內存比較小的情況。
可以拷貝索引以用於加速:
Directory ramDir = new RAMDirectory(otherDir);
或者
Directory.copy(Directory sourceDir,
Directory destDir,
boolean closeDirSrc);
2.11 線程安全、鎖
線程、多JVM安全
任意多個IndexReaders可同時打開,可以跨JVM。
同一時間 ,只能打開一個 IndexWriter,獨佔寫鎖 。內建線程安全機制。
IndexReaders可以在IndexWriter打開的時候打開。
多線程間可共享IndexReader或者IndexWriter,他們是線程安全的,內建同步機制且性能較高。
通過遠程文件系統共享IndexWriter
注意不要反覆打開、關閉,否則會影響性能。
Index的鎖
以文件鎖的形式,名爲write.lock。
如果在已經被鎖定的情況下再創建一個IndexWriter,會遇到LockObtainFailedException。
也支持其他鎖定方式,但是一般情況下無需改變它們。
IndexWriter.isLocked(Directory):檢查某目錄是否被鎖。
IndexWriter.unlock(Directory):對某目錄解鎖,危險!。
注意!每次IndexWriter無論執行了什麼操作,都要顯示的close !不會自動釋放鎖的!
2.12 調試索引
2.14 高級的索引選項
IndexReader可以用來徹底刪除已經去除的Index,優點如下:
1.通過Document的具體Number來刪除,更精確而IndexWriter不行。
2.IndexReader可以在刪除後立即顯示出來,而IndexWriter必須重新打開才能顯示出來。
3.IndexReader擁有undeleteAll,可以撤銷所有刪除的索引(只對尚未merged的有效 )。
釋放刪除索引後的空間
可以調用expungeDeletes顯示的釋放空間,它將執行merge從而釋放刪除但僅僅做了標記,尚未釋放的空間。
緩存和刷新
當添加索引、刪除索引時候,在內存中建立了一個緩存以減少磁盤I/O,Lucene會定期把這些緩存中的改動放入Directory中便形成了一個segment (段)。
IndexWriter刷新緩存的條件是:
當內存中數據已經大於setRAMBufferSizeMB的指定。
當索引中的Document數量多於setMaxBufferedDocs的指定。
當索引被刪除的數量多於setMaxBufferedDeleteTerms的指定。
上述條件之一發生時,即觸發緩存刷進,它將建立新的Segment但不存入磁盤,只有當commit後才寫入磁盤的index。
索引的commit
commit將改動持久化到本次索引中。只有調用commit後,再打開的IndexReader或者IndexSearcher才能看到最近一次commit之後的結果。
關閉close也將間接調用commit。
與commit相對的是rollback方法,它將撤銷上次commit之後的所有改動。
commit非常耗時,不能經常調用。
“雙緩衝”的commit
在圖形界面開發中,經常有雙緩衝技術,即一個用於被刷新,一個用於顯示,兩個之間互換使用。Lucene也支持這樣的機制。
Lucene暴露了兩個接口:
prepareCommit
Commit
prepareCommit比較慢,而調用prepareCommit後再調用Commit則會非常快。
刪除策略
IndexDeletionPolicy決定了刪除策略。可以決定是否保留之前的commit版本。
Lucene對ACID的事務支持
這主要是通過“同時只能打開一個IndexWriter”來實現的。
如果JVM、OS或者機器掛了,Lucene會自動恢復到上一個commit版本。
合併Merge
當索引有過多的Segnmnet的時候,需要進行合併Merge。優點:
1.減少了Segnment的文件數量
2.減少索引文件佔用的空間大小。
MERGEPOLICY決定何時需要執行合併Merge
MERGEPOLICY
選擇那些文件需要被合併,默認有兩種策略:
LogByteSizeMergePolicy :根據Index大小決定是否需要合併
LogDocMergePolicy :根據Document的數量決定是否需要合併
分別通過
setMergeFactor
和setMaxMergeDocs來指定,具體參數見API。
MERGESCHEDULER
決定如何進行合併:
ConcurrentMergeScheduler,後臺額外線程進行合併,可通過waitForMerges得知合併完成。
SerialMergeScheduler,在addDocument時候串行合併,使用統一線程。