Lucene學習總結之三:Lucene的索引文件格式(2)

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

四、具體格式

上面曾經交代過,Lucene保存了從Index到Segment到Document到Field一直到Term的正向信息,也包括了從Term到Document映射的反向信息,還有其他一些Lucene特有的信息。下面對這三種信息一一介紹。

4.1. 正向信息

Index –> Segments (segments.gen, segments_N) –> Field(fnm, fdx, fdt) –> Term (tvx, tvd, tvf)

上面的層次結構不是十分的準確,因為segments.gen和segments_N保存的是段(segment)的元數據信息(metadata),其實是每個Index一個的,而段的真正的數據信息,是保存在域(Field)和詞(Term)中的。

4.1.1. 段的元數據信息(segments_N)

一個索引(Index)可以同時存在多個segments_N(至于如何存在多個segments_N,在描述完詳細信息之后會舉例說明),然而當我們要打開一個索引的時候,我們必須要選擇一個來打開,那如何選擇哪個segments_N呢?

Lucene采取以下過程:

  • 其一,在所有的segments_N中選擇N最大的一個。基本邏輯參照SegmentInfos.getCurrentSegmentGeneration(File[] files),其基本思路就是在所有以segments開頭,并且不是segments.gen的文件中,選擇N最大的一個作為genA。
  • 其二,打開segments.gen,其中保存了當前的N值。其格式如下,讀出版本號(Version),然后再讀出兩個N,如果兩者相等,則作為genB。
  • segments.gen

    IndexInput genInput = directory.openInput(IndexFileNames.SEGMENTS_GEN);//"segments.gen"?
    int version = genInput.readInt();//讀出版本號?
    if (version == FORMAT_LOCKLESS) {//如果版本號正確?
    ??? long gen0 = genInput.readLong();//讀出第一個N?
    ??? long gen1 = genInput.readLong();//讀出第二個N?
    ??? if (gen0 == gen1) {//如果兩者相等則為genB?
    ??????? genB = gen0;?
    ??? }?
    }

  • 其三,在上述得到的genA和genB中選擇最大的那個作為當前的N,方才打開segments_N文件。其基本邏輯如下:

    if (genA > genB)?
    ??? gen = genA;?
    else?
    ??? gen = genB;

?

如下圖是segments_N的具體格式:

segments_N

  • Format:
    • 索引文件格式的版本號。
    • 由于Lucene是在不斷開發過程中的,因而不同版本的Lucene,其索引文件格式也不盡相同,于是規定一個版本號。
    • Lucene 2.1此值-3,Lucene 2.9時,此值為-9。
    • 當用某個版本號的IndexReader讀取另一個版本號生成的索引的時候,會因為此值不同而報錯。
  • Version:
    • 索引的版本號,記錄了IndexWriter將修改提交到索引文件中的次數。
    • 其初始值大多數情況下從索引文件里面讀出,僅僅在索引開始創建的時候,被賦予當前的時間,已取得一個唯一值。
    • 其值改變在IndexWriter.commit->IndexWriter.startCommit->SegmentInfos.prepareCommit->SegmentInfos.write->writeLong(++version)
    • 其初始值之所最初取一個時間,是因為我們并不關心IndexWriter將修改提交到索引的具體次數,而更關心到底哪個是最新的。IndexReader中常比較自己的version和索引文件中的version是否相同來判斷此IndexReader被打開后,還有沒有被IndexWriter更新。

//在DirectoryReader中有一下函數。

public boolean isCurrent() throws CorruptIndexException, IOException {?
? return SegmentInfos.readCurrentVersion(directory) == segmentInfos.getVersion();?
}

  • NameCount
    • 是下一個新段(Segment)的段名。
    • 所有屬于同一個段的索引文件都以段名作為文件名,一般為_0.xxx, _0.yyy,? _1.xxx, _1.yyy ……
    • 新生成的段的段名一般為原有最大段名加一。
    • 如同的索引,NameCount讀出來是2,說明新的段為_2.xxx, _2.yyy

image

  • SegCount
    • 段(Segment)的個數。
    • 如上圖,此值為2。
  • SegCount個段的元數據信息:
    • SegName
      • 段名,所有屬于同一個段的文件都有以段名作為文件名。
      • 如上圖,第一個段的段名為"_0",第二個段的段名為"_1"
    • SegSize
      • 此段中包含的文檔數
      • 然而此文檔數是包括已經刪除,又沒有optimize的文檔的,因為在optimize之前,Lucene的段中包含了所有被索引過的文檔,而被刪除的文檔是保存在.del文件中的,在搜索的過程中,是先從段中讀到了被刪除的文檔,然后再用.del中的標志,將這篇文檔過濾掉。
      • 如下的代碼形成了上圖的索引,可以看出索引了兩篇文檔形成了_0段,然后又刪除了其中一篇,形成了_0_1.del,又索引了兩篇文檔形成_1段,然后又刪除了其中一篇,形成_1_1.del。因而在兩個段中,此值都是2。

IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);?
writer.setUseCompoundFile(false);?
indexDocs(writer, docDir);//docDir中只有兩篇文檔

//文檔一為:Students should be allowed to go out with their friends, but not allowed to drink beer.

//文檔二為:My friend Jerry went to school to see his students but found them drunk which is not allowed.

writer.commit();//提交兩篇文檔,形成_0段。

writer.deleteDocuments(new Term("contents", "school"));//刪除文檔二?
writer.commit();//提交刪除,形成_0_1.del?
indexDocs(writer, docDir);//再次索引兩篇文檔,Lucene不能判別文檔與文檔的不同,因而算兩篇新的文檔。?
writer.commit();//提交兩篇文檔,形成_1段?
writer.deleteDocuments(new Term("contents", "school"));//刪除第二次添加的文檔二?
writer.close();//提交刪除,形成_1_1.del

  • ?
    • DelGen
      • .del文件的版本號
      • Lucene中,在optimize之前,刪除的文檔是保存在.del文件中的。
      • 在Lucene 2.9中,文檔刪除有以下幾種方式:
        • IndexReader.deleteDocument(int docID)是用IndexReader按文檔號刪除。
        • IndexReader.deleteDocuments(Term term)是用IndexReader刪除包含此詞(Term)的文檔。
        • IndexWriter.deleteDocuments(Term term)是用IndexWriter刪除包含此詞(Term)的文檔。
        • IndexWriter.deleteDocuments(Term[] terms)是用IndexWriter刪除包含這些詞(Term)的文檔。
        • IndexWriter.deleteDocuments(Query query)是用IndexWriter刪除能滿足此查詢(Query)的文檔。
        • IndexWriter.deleteDocuments(Query[] queries)是用IndexWriter刪除能滿足這些查詢(Query)的文檔。
        • 原來的版本中Lucene的刪除一直是由IndexReader來完成的,在Lucene 2.9中雖可以用IndexWriter來刪除,但是其實真正的實現是在IndexWriter中,保存了readerpool,當IndexWriter向索引文件提交刪除的時候,仍然是從readerpool中得到相應的IndexReader,并用IndexReader來進行刪除的。下面的代碼可以說明:

IndexWriter.applyDeletes()

-> DocumentsWriter.applyDeletes(SegmentInfos)

???? -> reader.deleteDocument(doc);

  • ?
    • ?
      • ?
        • DelGen是每當IndexWriter向索引文件中提交刪除操作的時候,加1,并生成新的.del文件。

IndexWriter.commit()

-> IndexWriter.applyDeletes()

??? -> IndexWriter$ReaderPool.release(SegmentReader)

???????? -> SegmentReader(IndexReader).commit()

???????????? -> SegmentReader.doCommit(Map)

????????????????? -> SegmentInfo.advanceDelGen()

?????????????????????? -> if (delGen == NO) {?
????????????????????????????? delGen = YES;?
?????????????????????????? } else {?
????????????????????????????? delGen++;?
?????????????????????????? }

IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);?
writer.setUseCompoundFile(false);

indexDocs(writer, docDir);//索引兩篇文檔,一篇包含"school",另一篇包含"beer"?
writer.commit();//提交兩篇文檔到索引文件,形成段(Segment) "_0"?
writer.deleteDocuments(new Term("contents", "school"));//刪除包含"school"的文檔,其實是刪除了兩篇文檔中的一篇。?
writer.commit();//提交刪除到索引文件,形成"_0_1.del"?
writer.deleteDocuments(new Term("contents", "beer"));//刪除包含"beer"的文檔,其實是刪除了兩篇文檔中的另一篇。?
writer.commit();//提交刪除到索引文件,形成"_0_2.del"?
indexDocs(writer, docDir);//索引兩篇文檔,和上次的文檔相同,但是Lucene無法區分,認為是另外兩篇文檔。?
writer.commit();//提交兩篇文檔到索引文件,形成段"_1"?
writer.deleteDocuments(new Term("contents", "beer"));//刪除包含"beer"的文檔,其中段"_0"已經無可刪除,段"_1"被刪除一篇。?
writer.close();//提交刪除到索引文件,形成"_1_1.del"

形成的索引文件如下:

image

?

  • ?
    • DocStoreOffset
    • DocStoreSegment
    • DocStoreIsCompoundFile
      • 對于域(Stored Field)和詞向量(Term Vector)的存儲可以有不同的方式,即可以每個段(Segment)單獨存儲自己的域和詞向量信息,也可以多個段共享域和詞向量,把它們存儲到一個段中去。
      • 如果DocStoreOffset為-1,則此段單獨存儲自己的域和詞向量,從存儲文件上來看,如果此段段名為XXX,則此段有自己的XXX.fdt,XXX.fdx,XXX.tvf,XXX.tvd,XXX.tvx文件。DocStoreSegment和DocStoreIsCompoundFile在此處不被保存。
      • 如果DocStoreOffset不為-1,則DocStoreSegment保存了共享的段的名字,比如為YYY,DocStoreOffset則為此段的域及詞向量信息在共享段中的偏移量。則此段沒有自己的XXX.fdt,XXX.fdx,XXX.tvf,XXX.tvd,XXX.tvx文件,而是將信息存放在共享段的YYY.fdt,YYY.fdx,YYY.tvf,YYY.tvd,YYY.tvx文件中。
      • DocumentsWriter中有兩個成員變量:String segment是當前索引信息存放的段,String docStoreSegment是域和詞向量信息存儲的段。兩者可以相同也可以不同,決定了域和詞向量信息是存儲在本段中,還是和其他的段共享。
      • IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)中第二個參數flushDocStores會影響到是否單獨或是共享存儲。其實最終影響的是DocumentsWriter.closeDocStore()。每當flushDocStores為false時,closeDocStore不被調用,說明下次添加到索引文件中的域和詞向量信息是同此次共享一個段的。直到flushDocStores為true的時候,closeDocStore被調用,從而下次添加到索引文件中的域和詞向量信息將被保存在一個新的段中,不同此次共享一個段(在這里需要指出的是Lucene的一個很奇怪的實現,雖然下次域和詞向量信息是被保存到新的段中,然而段名卻是這次被確定了的,在initSegmentName中當docStoreSegment == null時,被置為當前的segment,而非下一個新的segment,docStoreSegment = segment,于是會出現如下面的例子的現象)。
      • 好在共享域和詞向量存儲并不是經常被使用到,實現也或有缺陷,暫且解釋到此。

????? IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);?
????? writer.setUseCompoundFile(false);

????
????? indexDocs(writer, docDir);?
????? writer.flush();

//flush生成segment "_0",并且flush函數中,flushDocStores設為false,也即下個段將同本段共享域和詞向量信息,這時DocumentsWriter中的docStoreSegment= "_0"。

????? indexDocs(writer, docDir);?
????? writer.commit();

//commit生成segment "_1",由于上次flushDocStores設為false,于是段"_1"的域以及詞向量信息是保存在"_0"中的,在這個時刻,段"_1"并不生成自己的"_1.fdx"和"_1.fdt"。然而在commit函數中,flushDocStores設為true,也即下個段將單獨使用新的段來存儲域和詞向量信息。然而這時,DocumentsWriter中的docStoreSegment= "_1",也即當段"_2"存儲其域和詞向量信息的時候,是存在"_1.fdx"和"_1.fdt"中的,而段"_1"的域和詞向量信息卻是存在"_0.fdt"和"_0.fdx"中的,這一點非常令人困惑。 如圖writer.commit的時候,_1.fdt和_1.fdx并沒有形成。

image

????? indexDocs(writer, docDir);?
????? writer.flush();

//段"_2"形成,由于上次flushDocStores設為true,其域和詞向量信息是新創建一個段保存的,卻是保存在_1.fdt和_1.fdx中的,這時候才產生了此二文件。

image

????? indexDocs(writer, docDir);?
????? writer.flush();

//段"_3"形成,由于上次flushDocStores設為false,其域和詞向量信息是共享一個段保存的,也是是保存在_1.fdt和_1.fdx中的

????? indexDocs(writer, docDir);?
????? writer.commit();

//段"_4"形成,由于上次flushDocStores設為false,其域和詞向量信息是共享一個段保存的,也是是保存在_1.fdt和_1.fdx中的。然而函數commit中flushDocStores設為true,也意味著下一個段將新創建一個段保存域和詞向量信息,此時DocumentsWriter中docStoreSegment= "_4",也表明了雖然段"_4"的域和詞向量信息保存在了段"_1"中,將來的域和詞向量信息卻要保存在段"_4"中。此時"_4.fdx"和"_4.fdt"尚未產生。???

image

????? indexDocs(writer, docDir);?
????? writer.flush();

//段"_5"形成,由于上次flushDocStores設為true,其域和詞向量信息是新創建一個段保存的,卻是保存在_4.fdt和_4.fdx中的,這時候才產生了此二文件。

image

????? indexDocs(writer, docDir);?
????? writer.commit();?
????? writer.close();

//段"_6"形成,由于上次flushDocStores設為false,其域和詞向量信息是共享一個段保存的,也是是保存在_4.fdt和_4.fdx中的

image

  • ?
    • HasSingleNormFile
      • 在搜索的過程中,標準化因子(Normalization Factor)會影響文檔最后的評分。
      • 不同的文檔重要性不同,不同的域重要性也不同。因而每個文檔的每個域都可以有自己的標準化因子。
      • 如果HasSingleNormFile為1,則所有的標準化因子都是存在.nrm文件中的。
      • 如果HasSingleNormFile不是1,則每個域都有自己的標準化因子文件.fN
    • NumField
      • 域的數量
    • NormGen
      • 如果每個域有自己的標準化因子文件,則此數組描述了每個標準化因子文件的版本號,也即.fN的N。
    • IsCompoundFile
      • 是否保存為復合文件,也即把同一個段中的文件按照一定格式,保存在一個文件當中,這樣可以減少每次打開文件的個數。
      • 是否為復合文件,由接口IndexWriter.setUseCompoundFile(boolean)設定。?
      • 非符合文件同符合文件的對比如下圖:
非復合文件:?
image
復合文件:?
image

?

  • ?
    • DeletionCount
      • 記錄了此段中刪除的文檔的數目。
    • HasProx
      • 如果至少有一個段omitTf為false,也即詞頻(term freqency)需要被保存,則HasProx為1,否則為0。
    • Diagnostics
      • 調試信息。
  • User map data
    • 保存了用戶從字符串到字符串的映射Map
  • CheckSum
    • 此文件segment_N的校驗和。

讀取此文件格式參考SegmentInfos.read(Directory directory, String segmentFileName):

  • int format = input.readInt();
  • version = input.readLong(); // read version
  • counter = input.readInt(); // read counter
  • for (int i = input.readInt(); i > 0; i--) // read segmentInfos
    • add(new SegmentInfo(directory, format, input));
      • name = input.readString();
      • docCount = input.readInt();
      • delGen = input.readLong();
      • docStoreOffset = input.readInt();
      • docStoreSegment = input.readString();
      • docStoreIsCompoundFile = (1 == input.readByte());
      • hasSingleNormFile = (1 == input.readByte());
      • int numNormGen = input.readInt();
      • normGen = new long[numNormGen];
      • for(int j=0;j
      • normGen[j] = input.readLong();
    • isCompoundFile = input.readByte();
    • delCount = input.readInt();
    • hasProx = input.readByte() == 1;
    • diagnostics = input.readStringStringMap();
  • userData = input.readStringStringMap();
  • final long checksumNow = input.getChecksum();
  • final long checksumThen = input.readLong();

?

4.1.2. 域(Field)的元數據信息(.fnm)

一個段(Segment)包含多個域,每個域都有一些元數據信息,保存在.fnm文件中,.fnm文件的格式如下:

fnm

  • FNMVersion
    • 是fnm文件的版本號,對于Lucene 2.9為-2
  • FieldsCount
    • 域的數目
  • 一個數組的域(Fields)
    • FieldName:域名,如"title","modified","content"等。
    • FieldBits:一系列標志位,表明對此域的索引方式
      • 最低位:1表示此域被索引,0則不被索引。所謂被索引,也即放到倒排表中去。
        • 僅僅被索引的域才能夠被搜到。
        • Field.Index.NO則表示不被索引。
        • Field.Index.ANALYZED則表示不但被索引,而且被分詞,比如索引"hello world"后,無論是搜"hello",還是搜"world"都能夠被搜到。
        • Field.Index.NOT_ANALYZED表示雖然被索引,但是不分詞,比如索引"hello world"后,僅當搜"hello world"時,能夠搜到,搜"hello"和搜"world"都搜不到。
        • 一個域出了能夠被索引,還能夠被存儲,僅僅被存儲的域是搜索不到的,但是能通過文檔號查到,多用于不想被搜索到,但是在通過其它域能夠搜索到的情況下,能夠隨著文檔號返回給用戶的域。
        • Field.Store.Yes則表示存儲此域,Field.Store.NO則表示不存儲此域。
      • 倒數第二位:1表示保存詞向量,0為不保存詞向量。
        • Field.TermVector.YES表示保存詞向量。
        • Field.TermVector.NO表示不保存詞向量。
      • 倒數第三位:1表示在詞向量中保存位置信息。
        • Field.TermVector.WITH_POSITIONS
      • 倒數第四位:1表示在詞向量中保存偏移量信息。
        • Field.TermVector.WITH_OFFSETS
      • 倒數第五位:1表示不保存標準化因子
        • Field.Index.ANALYZED_NO_NORMS
        • Field.Index.NOT_ANALYZED_NO_NORMS
      • 倒數第六位:是否保存payload

要了解域的元數據信息,還要了解以下幾點:

  • 位置(Position)和偏移量(Offset)的區別
    • 位置是基于詞Term的,偏移量是基于字母或漢字的。

clip_image002

  • 索引域(Indexed)和存儲域(Stored)的區別
    • 一個域為什么會被存儲(store)而不被索引(Index)呢?在一個文檔中的所有信息中,有這樣一部分信息,可能不想被索引從而可以搜索到,但是當這個文檔由于其他的信息被搜索到時,可以同其他信息一同返回。
    • 舉個例子,讀研究生時,您好不容易寫了一篇論文交給您的導師,您的導師卻要他所第一作者而您做第二作者,然而您導師不想別人在論文系統中搜索您的名字時找到這篇論文,于是在論文系統中,把第二作者這個Field的Indexed設為false,這樣別人搜索您的名字,永遠不知道您寫過這篇論文,只有在別人搜索您導師的名字從而找到您的文章時,在一個角落表述著第二作者是您。
  • payload的使用
    • 我們知道,索引是以倒排表形式存儲的,對于每一個詞,都保存了包含這個詞的一個鏈表,當然為了加快查詢速度,此鏈表多用跳躍表進行存儲。
    • Payload信息就是存儲在倒排表中的,同文檔號一起存放,多用于存儲與每篇文檔相關的一些信息。當然這部分信息也可以存儲域里(stored Field),兩者從功能上基本是一樣的,然而當要存儲的信息很多的時候,存放在倒排表里,利用跳躍表,有利于大大提高搜索速度。
    • Payload的存儲方式如下圖:

payload

  • ?
    • Payload主要有以下幾種用法:
      • 存儲每個文檔都有的信息:比如有的時候,我們想給每個文檔賦一個我們自己的文檔號,而不是用Lucene自己的文檔號。于是我們可以聲明一個特殊的域(Field)"_ID"和特殊的詞(Term)"_ID",使得每篇文檔都包含詞"_ID",于是在詞"_ID"的倒排表里面對于每篇文檔又有一項,每一項都有一個payload,于是我們可以在payload里面保存我們自己的文檔號。每當我們得到一個Lucene的文檔號的時候,就能從跳躍表中查找到我們自己的文檔號。
//聲明一個特殊的域和特殊的詞?

public static final String ID_PAYLOAD_FIELD = "_ID";

public static final String ID_PAYLOAD_TERM = "_ID";

public static final Term ID_TERM = new Term(ID_PAYLOAD_TERM, ID_PAYLOAD_FIELD);

//聲明一個特殊的TokenStream,它只生成一個詞(Term),就是那個特殊的詞,在特殊的域里面。

static class SinglePayloadTokenStream extends TokenStream {?
??? private Token token;?
??? private boolean returnToken = false;

??? SinglePayloadTokenStream(String idPayloadTerm) {?
??????? char[] term = idPayloadTerm.toCharArray();?
??????? token = new Token(term, 0, term.length, 0, term.length);?
??? }

??? void setPayloadValue(byte[] value) {?
??????? token.setPayload(new Payload(value));?
??????? returnToken = true;?
??? }

??? public Token next() throws IOException {?
??????? if (returnToken) {?
??????????? returnToken = false;?
??????????? return token;?
??????? } else {?
??????????? return null;?
??????? }?
??? }?
}

//對于每一篇文檔,都讓它包含這個特殊的詞,在特殊的域里面

SinglePayloadTokenStream singlePayloadTokenStream = new SinglePayloadTokenStream(ID_PAYLOAD_TERM);?
singlePayloadTokenStream.setPayloadValue(long2bytes(id));?
doc.add(new Field(ID_PAYLOAD_FIELD, singlePayloadTokenStream));

//每當得到一個Lucene的文檔號時,通過以下的方式得到payload里面的文檔號?

long id = 0;?
TermPositions tp = reader.termPositions(ID_PAYLOAD_TERM);?
boolean ret = tp.skipTo(docID);?
tp.nextPosition();?
int payloadlength = tp.getPayloadLength();?
byte[] payloadBuffer = new byte[payloadlength];?
tp.getPayload(payloadBuffer, 0);?
id = bytes2long(payloadBuffer);?
tp.close();

?

  • ?
    • ?
      • 影響詞的評分
        • 在Similarity抽象類中有函數public float scorePayload(byte [] payload, int offset, int length)? 可以根據payload的值影響評分。
  • 讀取域元數據信息的代碼如下:

FieldInfos.read(IndexInput, String)

  • int firstInt = input.readVInt();
  • size = input.readVInt();
  • for (int i = 0; i < size; i++)
    • String name = input.readString();
    • byte bits = input.readByte();
    • boolean isIndexed = (bits & IS_INDEXED) != 0;
    • boolean storeTermVector = (bits & STORE_TERMVECTOR) != 0;
    • boolean storePositionsWithTermVector = (bits & STORE_POSITIONS_WITH_TERMVECTOR) != 0;
    • boolean storeOffsetWithTermVector = (bits & STORE_OFFSET_WITH_TERMVECTOR) != 0;
    • boolean omitNorms = (bits & OMIT_NORMS) != 0;
    • boolean storePayloads = (bits & STORE_PAYLOADS) != 0;
    • boolean omitTermFreqAndPositions = (bits & OMIT_TERM_FREQ_AND_POSITIONS) != 0;

?

4.1.3. 域(Field)的數據信息(.fdt,.fdx)

fdxfdt

  • 域數據文件(fdt):
    • 真正保存存儲域(stored field)信息的是fdt文件
    • 在一個段(segment)中總共有segment size篇文檔,所以fdt文件中共有segment size個項,每一項保存一篇文檔的域的信息
    • 對于每一篇文檔,一開始是一個fieldcount,也即此文檔包含的域的數目,接下來是fieldcount個項,每一項保存一個域的信息。
    • 對于每一個域,fieldnum是域號,接著是一個8位的byte,最低一位表示此域是否分詞(tokenized),倒數第二位表示此域是保存字符串數據還是二進制數據,倒數第三位表示此域是否被壓縮,再接下來就是存儲域的值,比如new Field("title", "lucene in action", Field.Store.Yes, …),則此處存放的就是"lucene in action"這個字符串。
  • 域索引文件(fdx)
    • 由域數據文件格式我們知道,每篇文檔包含的域的個數,每個存儲域的值都是不一樣的,因而域數據文件中segment size篇文檔,每篇文檔占用的大小也是不一樣的,那么如何在fdt中辨別每一篇文檔的起始地址和終止地址呢,如何能夠更快的找到第n篇文檔的存儲域的信息呢?就是要借助域索引文件。
    • 域索引文件也總共有segment size個項,每篇文檔都有一個項,每一項都是一個long,大小固定,每一項都是對應的文檔在fdt文件中的起始地址的偏移量,這樣如果我們想找到第n篇文檔的存儲域的信息,只要在fdx中找到第n項,然后按照取出的long作為偏移量,就可以在fdt文件中找到對應的存儲域的信息。
  • 讀取域數據信息的代碼如下:

Document FieldsReader.doc(int n, FieldSelector fieldSelector)

  • long position = indexStream.readLong();//indexStream points to ".fdx"
  • fieldsStream.seek(position);//fieldsStream points to "fdt"
  • int numFields = fieldsStream.readVInt();
  • for (int i = 0; i < numFields; i++)
    • int fieldNumber = fieldsStream.readVInt();
    • byte bits = fieldsStream.readByte();
    • boolean compressed = (bits & FieldsWriter.FIELD_IS_COMPRESSED) != 0;
    • boolean tokenize = (bits & FieldsWriter.FIELD_IS_TOKENIZED) != 0;
    • boolean binary = (bits & FieldsWriter.FIELD_IS_BINARY) != 0;
    • if (binary)
      • int toRead = fieldsStream.readVInt();
      • final byte[] b = new byte[toRead];
      • fieldsStream.readBytes(b, 0, b.length);
      • if (compressed)
        • int toRead = fieldsStream.readVInt();
        • final byte[] b = new byte[toRead];
        • fieldsStream.readBytes(b, 0, b.length);
        • uncompress(b),
    • else
      • fieldsStream.readString()

4.1.3. 詞向量(Term Vector)的數據信息(.tvx,.tvd,.tvf)

termvector

詞向量信息是從索引(index)到文檔(document)到域(field)到詞(term)的正向信息,有了詞向量信息,我們就可以得到一篇文檔包含那些詞的信息。

  • 詞向量索引文件(tvx)
    • 一個段(segment)包含N篇文檔,此文件就有N項,每一項代表一篇文檔。
    • 每一項包含兩部分信息:第一部分是詞向量文檔文件(tvd)中此文檔的偏移量,第二部分是詞向量域文件(tvf)中此文檔的第一個域的偏移量。
  • 詞向量文檔文件(tvd)
    • 一個段(segment)包含N篇文檔,此文件就有N項,每一項包含了此文檔的所有的域的信息。
    • 每一項首先是此文檔包含的域的個數NumFields,然后是一個NumFields大小的數組,數組的每一項是域號。然后是一個(NumFields - 1)大小的數組,由前面我們知道,每篇文檔的第一個域在tvf中的偏移量在tvx文件中保存,而其他(NumFields - 1)個域在tvf中的偏移量就是第一個域的偏移量加上這(NumFields - 1)個數組的每一項的值。
  • 詞向量域文件(tvf)
    • 此文件包含了此段中的所有的域,并不對文檔做區分,到底第幾個域到第幾個域是屬于那篇文檔,是由tvx中的第一個域的偏移量以及tvd中的(NumFields - 1)個域的偏移量來決定的。
    • 對于每一個域,首先是此域包含的詞的個數NumTerms,然后是一個8位的byte,最后一位是指定是否保存位置信息,倒數第二位是指定是否保存偏移量信息。然后是NumTerms個項的數組,每一項代表一個詞(Term),對于每一個詞,由詞的文本TermText,詞頻TermFreq(也即此詞在此文檔中出現的次數),詞的位置信息,詞的偏移量信息。
  • 讀取詞向量數據信息的代碼如下:

TermVectorsReader.get(int docNum, String field, TermVectorMapper)

  • int fieldNumber = fieldInfos.fieldNumber(field);//通過field名字得到field號
  • seekTvx(docNum);//在tvx文件中按docNum文檔號找到相應文檔的項
  • long tvdPosition = tvx.readLong();//找到tvd文件中相應文檔的偏移量
  • tvd.seek(tvdPosition);//在tvd文件中按偏移量找到相應文檔的項
  • int fieldCount = tvd.readVInt();//此文檔包含的域的個數。
  • for (int i = 0; i < fieldCount; i++) //按域號查找域
    • number = tvd.readVInt();
    • if (number == fieldNumber)
      • found = i;
  • position = tvx.readLong();//在tvx中讀出此文檔的第一個域在tvf中的偏移量
  • for (int i = 1; i <= found; i++)
    • position += tvd.readVLong();//加上所要找的域在tvf中的偏移量
  • tvf.seek(position);
  • int numTerms = tvf.readVInt();
  • byte bits = tvf.readByte();
  • storePositions = (bits & STORE_POSITIONS_WITH_TERMVECTOR) != 0;
  • storeOffsets = (bits & STORE_OFFSET_WITH_TERMVECTOR) != 0;
  • for (int i = 0; i < numTerms; i++)
    • start = tvf.readVInt();
    • deltaLength = tvf.readVInt();
    • totalLength = start + deltaLength;
    • tvf.readBytes(byteBuffer, start, deltaLength);
    • term = new String(byteBuffer, 0, totalLength, "UTF-8");
    • if (storePositions)
      • positions = new int[freq];
      • int prevPosition = 0;
      • for (int j = 0; j < freq; j++)
        • positions[j] = prevPosition + tvf.readVInt();
        • prevPosition = positions[j];
    • if (storeOffsets)
      • offsets = new TermVectorOffsetInfo[freq];
      • int prevOffset = 0;
      • for (int j = 0; j < freq; j++)
      • int startOffset = prevOffset + tvf.readVInt();
      • int endOffset = startOffset + tvf.readVInt();
      • offsets[j] = new TermVectorOffsetInfo(startOffset, endOffset);
      • prevOffset = endOffset;

分類:?Lucene原理與代碼分析

轉載于:https://my.oschina.net/weiweiblog/blog/3018429

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/275683.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/275683.shtml
英文地址,請注明出處:http://en.pswp.cn/news/275683.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JavaScript 數組 API 全解析

在編程世界中&#xff0c;數組是指元素的集合。數組將數據作為元素進行存儲&#xff0c;并在需要時將其取出。在支持數組的編程語言中廣泛地采用了這個數據結構。這個手冊會介紹 JavaScript 數組的所有知識。你將會學到復雜數據處理、解構、常用數組方法等內容。我為什么寫這篇…

Spoken English(001)

if he wants to make any changes,minor alternations can be made thenis there any way of ensuring well have enough time for our talks?so our evenings will be quite full then?We’ll leave some evenings free,that is, if it is all right with you.We’d have to…

美學設計評價_死亡的孩子無法使用的設計美學

美學設計評價In the popular anime series, Soul Eater, Death the Kid is a Shinigami (Japanese death god) who vanquishes evil with his dual pistols, Liz and Patty. Although he’s strikingly powerful, his battles are often hindered by his obsessive-compulsive …

iis php網站500錯誤原因_因為曾經錯誤安裝過PHP5.2而導致IIS7無法正常工作,顯示500錯誤提示,大家幫忙看看!...

我的系統是VISTA,使用自帶的IIS7&#xff0c;對ASP網頁一直瀏覽正常。最近因自己心血來潮錯誤安裝了一次PHP5.2。主要操作是&#xff1a;1、下載的是自動安裝的PHP5.2程序&#xff0c;自動選ISAPI按鈕安裝&#xff1b;2、對IIS7中添加了名為“PHP”的ISAPI篩選器&#xff0c;選…

【送書】2021年哪些好書值得讀(小姐姐配音)

大家好&#xff0c;我是若川。記得點上面的小姐姐再次錄制的配音。為感謝大家一直以來的支持和肯定。不知道是今年第幾次送書了。昨天送書的音頻廣受好評&#xff0c;沒參與的可以參與。今天聯合華章圖書再送文中任選一本 * 3 包郵送&#xff0c;詳細規則看文末。Web開發01《斯…

Flash獲取html參數的方法

一. swf?傳參 html代碼:代碼<html><head><meta http-equiv”Content-Type” content”text/html charsetutf-8″ /><title>as</title></head><body><object classid”clsid:D27CDB6E-AE6D-11cf-96B8-444553540000″ codebase”h…

方法重載_方法

方法重載Recently, I wrote an article about moving XD designs to Figma. It was a really interesting experiment and one that seemed to interest quite a lot of people.最近&#xff0c;我寫了一篇有關將XD設計移至Figma的文章。 這是一個非常有趣的實驗&#xff0c;似…

php strtoup,PHP 7 的幾處函數安全小變化

To Begin With最近在準備 LANCTF&#xff0c;想把環境遷移到 PHP 7&#xff0c;卻想到一些 payload 失效了。想著什么時候總結成一個筆記&#xff0c;恰巧在 FB 發現有人寫了一篇博文&#xff0c;拜讀后結合 CTF 環境整理了一下&#xff0c;總體來說&#xff0c;棄用了較多不安…

Node.js 框架設計及企業 Node.js 基礎建設相關討論

大家好&#xff0c;我是若川。19年我寫的 lodash源碼 文章投稿到海鏡大神知乎專欄竟然通過了&#xff0c;后來20年海鏡大神還star了我的博客&#xff0c;同時還轉發了我的微博。時間真快啊。今天分享這篇Node.js的討論。2021 年上半年早已過去&#xff0c;回顧 Node.js 在國內的…

DAS、NAS、SAN、iSCSI 存儲方案概述

目前服務器所使用的專業存儲方案有DAS、NAS、SAN、iSCSI幾種。存儲根據服務器類型可以分為&#xff1a;封閉系統的存儲和開放系統的存儲&#xff1a; &#xff08;1&#xff09;封閉系統主要指大型機. &#xff08;2&#xff09;開放系統指基于包括Windows、UNIX、Linux等操作系…

同態加法_同態—當舊趨勢突然變酷時

同態加法Designers get excited at every year’s end to see what next year’s trend is going to be. What the future of design is going to look like. What they can carry forward to the next year; And Neumorphism was one among the lists which gained great atte…

網頁標題設置,為什么在SERP中,顯示結果不一致?

在網站建設與運營的過程中&#xff0c;我們經常會遇到各種各樣的問題&#xff0c;特別是關于網頁標題設置的問題&#xff0c;如果一個頁面標題出錯&#xff0c;那么&#xff0c;你整個頁面建設的過程&#xff0c;就完全是事倍功半&#xff0c;得不償失。 那么&#xff0c;網頁標…

阿里專有釘釘前端面試指南

大家好&#xff0c;我是若川。今天推薦這篇掘金高贊文章&#xff0c;歡迎留言交流。經作者子奕大佬授權轉載&#xff0c;原文鏈接&#xff1a;https://juejin.cn/post/6986436944913924103作者介紹子弈[1]&#xff0c;專有釘釘前端團隊成員&#xff0c;負責專有釘釘 PC 客戶端的…

安全態勢感知產品對比_設計中的對比和人的感知

安全態勢感知產品對比In this article, we’re going to explore the concept of contrast and its fundamental role in UX and visual design.在本文中&#xff0c;我們將探討對比度的概念及其在UX和視覺設計中的基本作用。 Let’s start by defining what contrast is.讓我…

在字節做前端一年后,有啥收獲~

大家好&#xff0c;我是若川。今天分享這篇&#xff0c;相信讀完會有些收獲。本文經作者授權轉載&#xff0c;原文鏈接&#xff1a;https://juejin.cn/post/6980671091526074404個人簡介19年底12月進入字節實習&#xff0c; 第二年7月畢業轉正。到前幾天正好全職一周年。進入公…

app用戶隱私協議相關法律_隱私圖標和法律設計

app用戶隱私協議相關法律During its 2020 Worldwide Developers Conference, Apple spent time on one of today’s hottest topics — privacy. During the past couple of years, Apple has been rolling out various public campaigns aiming to position itself as a compa…

HTTP referer

簡言之&#xff0c;HTTP Referer是header的一部分&#xff0c;當瀏覽器向web服務器發送請求的時候&#xff0c;一般會帶上Referer&#xff0c;告訴服務器我是從哪個頁面鏈接過來的&#xff0c;服務器籍此可以獲得一些信息用于處理。比如從我主頁上鏈接到一個朋友那里&#xff0…

ecshop模板支持php,[老楊原創]關于ECSHOP模板架設的服務器php版本過高報錯的解決方法集合...

1、admin/index.phpadmin/sms_url.php報錯&#xff1a;Strict Standards: mktime(): You should be using the time() function instead in /data/web/ledetaoadmin/sms_url.php on line 31$auth mktime();替換為&#xff1a;$auth time();報錯&#xff1a;Strict Standards:…

35 點擊全圖后發現地圖“不見了”

相信很多用ArcGIS軟件作圖的時候會習慣用全圖按鈕&#xff0c;但是有的時候工程文件是他人提供的&#xff0c;也不太清楚是怎么做的&#xff0c;一點全圖&#xff0c;軟件界面就一片空白&#xff0c;找數據找半天&#xff0c;很是苦惱啊 這雖然不是什么大問題&#xff0c;但還是…

成為優秀溝通者的要素_如果您想成為更好的設計師,請成為更好的溝通者

成為優秀溝通者的要素Little changes that go a long way.小變化大有幫助。 I started my career in motion design.我的職業生涯始于運動設計。 My focus was on the visual and technical skills required to make emotionally compelling work. I believed great design s…