Lucene索引文件組成(2)

4、具體格式

上面曾經交代過,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)app

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

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

一個索引(Index)能夠同時存在多個segments_N(至於如何存在多個segments_N,在描述完詳細信息以後會舉例說明),然而當咱們要打開一個索引的時候,咱們必需要選擇一個來打開,那如何選擇哪一個segments_N呢? spa

Lucene採起如下過程:3d

  • 其一,在全部的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;orm

 

以下圖是segments_N的具體格式:blog

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();
}token

  • 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文件的格式以下:

    • 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的,偏移量是基於字母或漢字的。

    • 索引域(Indexed)和存儲域(Stored)的區別
      • 一個域爲何會被存儲(store)而不被索引(Index)呢?在一個文檔中的全部信息中,有這樣一部分信息,可能不想被索引從而能夠搜索到,可是當這個文檔因爲其餘的信息被搜索到時,能夠同其餘信息一同返回。
      • 舉個例子,讀研究生時,您好不容易寫了一篇論文交給您的導師,您的導師卻要他所第一做者而您作第二做者,然而您導師不想別人在論文系統中搜索您的名字時找到這篇論文,因而在論文系統中,把第二做者這個Field的Indexed設爲false,這樣別人搜索您的名字,永遠不知道您寫過這篇論文,只有在別人搜索您導師的名字從而找到您的文章時,在一個角落表述着第二做者是您。
    • payload的使用
      • 咱們知道,索引是以倒排表形式存儲的,對於每個詞,都保存了包含這個詞的一個鏈表,固然爲了加快查詢速度,此鏈表多用跳躍表進行存儲。
      • Payload信息就是存儲在倒排表中的,同文檔號一塊兒存放,多用於存儲與每篇文檔相關的一些信息。固然這部分信息也能夠存儲域裏(stored Field),二者從功能上基本是同樣的,然而當要存儲的信息不少的時候,存放在倒排表裏,利用跳躍表,有利於大大提升搜索速度。
      • 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)

    • 域數據文件(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)

    詞向量信息是從索引(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;
    相關文章
    相關標籤/搜索