Lucene-2.3.1 源代码阅读学习(26)

xiaoxiao2022-06-16  38

如果在初始化一个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 Factors

关于不同格式的索引文件的内容示例

为了直观,写一个简单的例子:

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 Factors 。)

转载请注明原文地址: https://www.6miu.com/read-4940953.html

最新回复(0)