如果在初始化一個IndexWriter索引器的時候,指定 useCompoundFile =false,則在指定的索引目錄中生成的索引文件就不是.cfs復合索引文件。
通過這種方式生成的索引文件,它的不同格式表明了它鎖存儲的關于索引的不同內容。
至少,明確了在建立索引過程中,經過加工處理的數據究竟去向如何,能夠加深對Lucene索引過程的理解。
通過在文章 Lucene-2.2.0 源代碼閱讀學習(4) 中的那個例子,可以運行主函數,觀察到索引目錄中生成了大量的不同擴展名的索引文件,當然它們不是復合索引文件,如圖所示:
這些不同擴展名的索引文件都是有一定的含義的。
如果只是根據這些文件名來說明它的含義,讓人感覺很抽象,那么就通過代碼來看,它們到底都存儲了一些什么內容。
_N.fnm文件
當向一個IndexWriter索引器實例添加Document的時候,調用了IndexWroter的addDocument()方法,在方法的內部調用如下:
buildSingleDocSegment() —> String segmentName = newRamSegmentName();
這時,調用newRamSegmentName()方法生成了一個segment的名稱,形如_ram_N,這里N為36進制數。
這個新生成的segmentName作為參數值傳遞到DocumentWriter類的addDocument()方法中:
dw.addDocument(segmentName, doc);
在DocumentWriter類中,這個segmentName依然是_ram_N形式的,再次作為參數值傳遞:
fieldInfos.write(directory, segment + ".fnm");
這個時候,就要發生變化了,在FieldInfos類的第一個write()方法中輸出System.out.println(name);,結果如下所示:
_ram_0.fnm
_ram_1.fnm
_ram_2.fnm
_ram_3.fnm
_ram_4.fnm
_ram_5.fnm
_ram_6.fnm
_ram_7.fnm
_ram_8.fnm
_ram_9.fnm
_0.fnm
_ram_a.fnm
_ram_b.fnm
_ram_c.fnm
_ram_d.fnm
_ram_e.fnm
_ram_f.fnm
_ram_g.fnm
_ram_h.fnm
_ram_i.fnm
_ram_j.fnm
_1.fnm
_ram_k.fnm
……
而且,可以從Directory看出究竟在這個過程中發生了怎樣的切換過程,在FieldInfos類的第一個write()方法中執行:
??? if(d instanceof FSDirectory){
??? System.out.println("FSDirectory");
??? }
??? else{
??? System.out.println("----RAMDirectory");
??? }
輸出結果如下所示:
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
FSDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
FSDirectory
……
可以看出,每次處理過10個.fnm文件(文件全名_ram_N.fnm),是在RAMDirectory中,然后就切換到FSDirectory中,這時輸出到本地磁盤的索引目錄中的索引文件是_N.fnm,可以從上面的實例圖中看到_0.fnm、_1.fnm等等。
真正執行向_N.fnm文件中寫入內容是在FieldInfos類的第二個write()方法中,可以從該方法的實現來看到底都寫入了哪些內容:
public void write(IndexOutput output) throws IOException {
??? output.writeVInt(size());
??? for (int i = 0; i < size(); i++) {
????? FieldInfo fi = fieldInfo(i);
????? byte bits = 0x0;
????? if (fi.isIndexed) bits |= IS_INDEXED;
????? if (fi.storeTermVector) bits |= STORE_TERMVECTOR;
????? if (fi.storePositionWithTermVector) bits |= STORE_POSITIONS_WITH_TERMVECTOR;
????? if (fi.storeOffsetWithTermVector) bits |= STORE_OFFSET_WITH_TERMVECTOR;
????? if (fi.omitNorms) bits |= OMIT_NORMS;
????? if (fi.storePayloads) bits |= STORE_PAYLOADS;
????? output.writeString(fi.name);
????? output.writeByte(bits);
??? }
}
從后兩行代碼可以看出,首先寫入了一個Field的名稱(name),然后寫入了一個byte值。這個byte的值可以根據從該FieldInfos類定義的一些標志經過位運算得到,從而從FieldIno的實例中讀取Field的信息,根據Field的一些信息(如:是否被索引、是否存儲詞條向量等等)來設置byte bits,這些標志的定義為:
static final byte IS_INDEXED = 0x1;
static final byte STORE_TERMVECTOR = 0x2;
static final byte STORE_POSITIONS_WITH_TERMVECTOR = 0x4;
static final byte STORE_OFFSET_WITH_TERMVECTOR = 0x8;
static final byte OMIT_NORMS = 0x10;
static final byte STORE_PAYLOADS = 0x20;
_N.fdt文件和_N.fdx文件
接著,在DocumentWriter類中的addDocumet()方法中,根據Directory實例、segment的名稱、一個FieldInfos的實例構造了一個FieldsWriter類的實例:
FieldsWriter fieldsWriter =?? new FieldsWriter(directory, segment, fieldInfos);
可以從FieldsWriter類的構造方法可以看出,實際上,根據生成的segment的名稱(_ram_N和_N)創建了兩個輸出流對象:
??? FieldsWriter(Directory d, String segment, FieldInfos fn) throws IOException {
??????? fieldInfos = fn;????????
???????
fieldsStream
= d.createOutput(segment + ".fdt");
???????
indexStream
= d.createOutput(segment + ".fdx");
??? }
這時,_N.fdt和_N.fdx文件就要生成了。
繼續看DocumentWriter類中的addDocument()方法:
fieldsWriter.addDocument(doc);
這時進入到FieldsWriter類中了,在addDocument()方法中提取Field的信息,寫入到,_N.fdt和_N.fdx文件中。FieldsWriter類的addDocument()方法實現如下:
??? final void addDocument(Document doc) throws IOException {
???????
indexStream.writeLong(fieldsStream.getFilePointer());
??
// 向indexStream中(即_N.fdx文件)中寫入fieldsStream(_N.fdt文件)流中的當前位置,也就是寫入這個Field信息的位置
??????? int storedCount = 0;
??????? Iterator fieldIterator = doc.getFields().iterator();
??????? while (fieldIterator.hasNext()) {??
// 循環遍歷該Document中所有Field,統計需要存儲的Field的個數
??????????? Fieldable field = (Fieldable) fieldIterator.next();
??????????? if (field.isStored())
??????????????? storedCount++;
??????? }
??????
fieldsStream.writeVInt(storedCount);
??
// 存儲Document中需要存儲的的Field的個數,寫入到_N.fdt文件
??????? fieldIterator = doc.getFields().iterator();
??????? while (fieldIterator.hasNext()) {
??????????? Fieldable field = (Fieldable) fieldIterator.next();
??????????? // if the field as an instanceof FieldsReader.FieldForMerge, we're in merge mode
??????????? // and field.binaryValue() already returns the compressed value for a field
??????????? // with isCompressed()==true, so we disable compression in that case
??????????? boolean disableCompression = (field instanceof FieldsReader.FieldForMerge);
??????????? if (field.isStored()) {???
// 如果Field需要存儲,將該Field的編號寫入到_N.fdt文件
???????????????
fieldsStream.writeVInt(fieldInfos.fieldNumber(field.name()));
??????????????? byte bits = 0;
??????????????? if (field.isTokenized())
??????????????????? bits |= FieldsWriter.FIELD_IS_TOKENIZED;
??????????????? if (field.isBinary())
??????????????????? bits |= FieldsWriter.FIELD_IS_BINARY;
??????????????? if (field.isCompressed())
??????????????????? bits |= FieldsWriter.FIELD_IS_COMPRESSED;
???????????????
???????????????
fieldsStream.writeByte(bits);
??
// 將Field的是否分詞,或是否壓縮,或是否以二進制流存儲,這些信息都寫入到_N.fdt文件
????????????????
??????????????? if (field.isCompressed()) {
?????????????????
// 如果當前Field可以被壓縮
????????????????? byte[] data = null;
?????????????????
????????????????? if (disableCompression) {
?????????????????????
// 已經被壓縮過,科恩那個需要進行合并優化
????????????????????? data = field.binaryValue();
????????????????? } else {
?????????????????????
// 檢查Field是否以二進制存儲
????????????????????? if (field.isBinary()) {
??????????????????????? data = compress(field.binaryValue());
????????????????????? }
????????????????????? else {????
//?? 設置編碼方式,壓縮存儲處理
??????????????????????? data = compress(field.stringValue().getBytes("UTF-8"));
????????????????????? }
????????????????? }
????????????????? final int len = data.length;
?????????????????
fieldsStream.writeVInt(len);
???
// 寫入Field名稱(以二進制存儲)的長度到_N.fdt文件
?????????????????
fieldsStream.writeBytes(data, len);
// 通過字節流的方式,寫入Field名稱(以二進制存儲)到_N.fdt文件
??????????????? }
??????????????? else {
?????????????????
// 如果當前這個Field不能進行壓縮
????????????????? if (field.isBinary()) {
??????????????????? byte[] data = field.binaryValue();
??????????????????? final int len = data.length;
???????????????????
fieldsStream.writeVInt(len);
??????????????????? fieldsStream.writeBytes(data, len);
????????????????? }
????????????????? else {
???????????????????
fieldsStream.writeString(field.stringValue());
???
// 如果Field不是以二進制存儲,則以String的格式寫入到_N.fdt文件
????????????????? }
??????????????? }
??????????? }
??????? }
??? }
從該方法可以看出:
_N.fdx文件(即indexStream流)中寫入的內容是:一個Field在_N.fdt文件中位置。
_N.fdt文件(即fieldsStream流)中寫入的內容是:
(1) Document中需要存儲的Field的數量;
(2) 每個Field在Document中的編號;
(3) 每個Field關于是否分詞、是否壓縮、是否以二進制存儲這三個指標的一個組合值;
(4) 每個Field的長度;
(5) 每個Field的內容(binaryValue或stringValue);
_N.frq文件和_N.prx文件
仍然在DocumentWriter類的addDocument()方法中看:
writePostings(postings, segment);
因為在調用該方法之前,已經對Documeng進行了倒排,在倒排的過程中對Document中的Field進行了處理,如果Field指定了要進行分詞,則在倒排的時候進行了分詞處理,這時生成了詞條。然后調用writePostings()方法,根據生成的segment的名稱_ram_N,設置詞條的頻率、位置等信息,并寫入到索引目錄中。
在writePostings()方法中,首先創建了兩個輸出流:
????? freq = directory.createOutput(segment + ".frq");
????? prox = directory.createOutput(segment + ".prx");
這時,_N.frq文件和_N.prx文件就要在索引目錄中生成了。
經過倒排,各個詞條的重要信息都被存儲到了Posting對象中,Posting類是為詞條的信息服務的。因此,在writePostings()方法中可以遍歷Posting[]數組中的各個Posting實例,讀取并處理這些信息,然后輸出到索引目錄中。
設置_N.frq文件的起始寫入內容:
??????? int postingFreq = posting.freq;
??????? if (postingFreq == 1)?????
// 如果該詞條第一次出現造Document中
????????? freq.writeVInt(1);????
// 頻率色繪制為1
??????? else {
????????? freq.writeVInt(0);????
// 如果不是第一次出現,對應的Document的編號0要寫入到_N.frq文件
????????? freq.writeVInt(postingFreq);????
// 設置一個詞條在該Document中的頻率值
??????? }
再看prox輸出流:
??????????? if (payloadLength == lastPayloadLength) {????
// 其中,int lastPayloadLength = -1;
?????????????
// the length of the current payload equals the length
??????????? // of the previous one. So we do not have to store the length
??????????? // again and we only shift the position delta by one bit
?????????????
prox.writeVInt(delta * 2);
???
//其中,int delta = position - lastPosition,int position = positions[j];
??????????? } else {
???????????
// the length of the current payload is different from the
??????????? // previous one. We shift the position delta, set the lowest
??????????? // bit and store the current payload length as VInt.
????????????
prox.writeVInt(delta * 2 + 1);
????????????? prox.writeVInt(payloadLength);
????????????? lastPayloadLength = payloadLength;
??????????? }
??????????? if (payloadLength > 0) {
???????????
// write current payload
?????????????
prox.writeBytes(payload.data, payload.offset, payload.length);
??????????? }
????????? } else {
?????????
// field does not store payloads, just write position delta as VInt
???????????
prox.writeVInt(delta);
????????? }
一個Posting包含了關于一個詞條在一個Document中出現的所有位置(用一個int[]數組來描述)、頻率(int)、該詞條對應的所有的Payload信息(用Payload[]來描述,因為一個詞條具有了頻率信息,自然就對應了多個Payload)。
關于Payload可以參考文章 Lucene-2.2.0 源代碼閱讀學習(23) 。
_N.prx文件文件寫入的內容都是與位置相關的數據。
從上面可以看出:
_N.frq文件(即freq流)中寫入的內容是:
(1) 一個詞條所在的Document的編號;
(2) 每個詞條在Document中頻率(即:出現的次數);
_N.prx文件(即prox流)中寫入的內容是:
其實主要就是Payload的信息,如:一個詞條對應的Payload的長度信息、起始偏移量信息;
_N.nrm文件
在DocumentWriter類的addDocument()方法中可以看到調用了writeNorms()方法:
writeNorms(segment);
也是根據生成的segment的名稱_ram_N來創建一個輸出流,看writeNorms()方法的定義:
private final void writeNorms(String segment) throws IOException {
??? for(int n = 0; n < fieldInfos.size(); n++){
????? FieldInfo fi = fieldInfos.fieldInfo(n);
????? if(fi.isIndexed && !fi.omitNorms){
??????? float norm = fieldBoosts[n] * similarity.lengthNorm(fi.name, fieldLengths[n]);
??????? IndexOutput norms = directory.createOutput(segment + ".f" + n);
??????? try {
????????? norms.writeByte(Similarity.encodeNorm(norm));
??????? } finally {
????????? norms.close();
??????? }
????? }
??? }
}
將一些標準化因子的信息,都寫入到了_N.nrm文件。其中每個segment對應著一個_N.nrm文件。
關于標準化因子可以參考文章 Lucene-2.2.0 源代碼閱讀學習(19) ,或者直接參考Apache官方網站 http://lucene.apache.org/java/docs/fileformats.html#Normalization%20Factors 。
關于不同格式的索引文件的內容示例
為了直觀,寫一個簡單的例子:
package org.shirdrn.lucene;
import java.io.IOException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
public class LuceneIndexFormat {
public static void main(String[] args) {
?? String indexPath = "E:\\Lucene\\myindex";
?? String maven = "Maven is a software project management and comprehension tool.";
?? String lucene = "Apache Lucene is a search engine written entirely in Java.";
?? Document doc1 = new Document();
?? doc1.add(new Field("maven",maven,Field.Store.YES,Field.Index.TOKENIZED));
?? Document doc2 = new Document();
?? doc2.add(new Field("lucene",lucene,Field.Store.YES,Field.Index.TOKENIZED));
?? try {
??? IndexWriter indexWriter = new IndexWriter(indexPath,new StandardAnalyzer(),true);
??? indexWriter.setUseCompoundFile(false);
??? indexWriter.addDocument(doc1);
??? indexWriter.addDocument(doc2);
??? indexWriter.close();
?? } catch (CorruptIndexException e) {
??? e.printStackTrace();
?? } catch (LockObtainFailedException e) {
??? e.printStackTrace();
?? } catch (IOException e) {
??? e.printStackTrace();
?? }
}
}
運行主函數后,在指定的索引目錄下生成了索引文件,而且是同一個索引段,如圖所示:
使用UltraEdit-32打開_0.fnm文件,可以看到內容如下所示:

就是我們在程序中設置的,即:
?? doc.add(new Field("maven",maven,Field.Store.YES,Field.Index.TOKENIZED));
?? doc.add(new Field("lucene",lucene,Field.Store.YES,Field.Index.TOKENIZED));
就是這兩個Field的name。
使用UltraEdit-32打開_0.fdt文件,可以看到內容如下所示:
其實,就是Field的內容。(上面的文本內容實際上存儲在一行)
使用UltraEdit-32打開_0.fdx文件,可以看到內容如下所示:

其實,就是在_0.fdt文件中,兩個Field的存放位置。
第一個Field是從0位置開始的,第二個是從42(這里是16進制,十進制為66)位置開始的。
使用UltraEdit-32打開_0.nrm文件,可以看到內容如下所示:

這里是標準化因子信息。
(關于標準化因子可以參考文章 Lucene-2.2.0 源代碼閱讀學習(19) ,或者直接參考Apache官方網站 http://lucene.apache.org/java/docs/fileformats.html#Normalization%20Factors 。)
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
