1.3.2 第二车间——DocInverterPerField
DocInverterPerField 负责对DocFieldProcessorPerThread对象的Fieldable[]数组的内容建立倒排索引,也就是处理同名字的所有Field。但实际上这个类主要解决的是前期工作,比如分词,统计位置信息等。倒排索引结构的核心的工作由TermsHashPerField和 FreqProxTermsWriterPerField (第三车间 ) 来完成。这两个类将在后面的专题中再提及。
DocInverterPerField 核心方法是processFields(Fieldable[] fields)。它负责这几个方面的工作:
(1)将field的value值切分成一个个term
(2)调用FieldInvertState类来存储和统计当前field的所有term出现的位置position和offset信息,并计算该field的boost分值,为所有相同名字的fields的boost与文档的boost的乘积。
(3) 调用TermsHashPerField和 FreqProxTermsWriterPerField 把 每个term 加入倒排索引结构。
Part I src code:
- public void processFields(final Fieldable[] fields, final int count) {
- //FieldInvertState类的职责就是跟踪将要加入索引(index)结构中的词语的位置(position/offset)
- //首先初始化FieldInvertState类的数据域。
- fieldState.reset(docState.doc.getBoost());
- //确定Field允许的最大词语数量10000
- final int maxFieldLength = docState.maxFieldLength;
- //确定fields数组是否需要索引(isIndexed)
- //如果有一个field需要索引,则doInvert=true
- final boolean doInvert = consumer.start(fields, count);
- //取出fields[]中的取出当前field(这些field名字相同)
- for(int i=0;i<count;i++) {
- final Fieldable field = fields[i];
- //当前field需要索引且整个fields数组都需要检索
- if (field.isIndexed() && doInvert) {
- //如果有多个同名的field,则将后面的field的value接到前面的field之后
- //即field[1]的第一个token的词语位置要从field[0]开始算起。
- if (fieldState.length > 0)
- fieldState.position += docState.analyzer.getPositionIncrementGap(fieldInfo.name);
- //当前field不需要分词
- if(!field.isTokenized()) {
- ....
- //则直接将整个field的值交给TermsHashPerField建立索引
- consumer.start(field);
- try {
- consumer.add();
- success = true;
- } finally {
- if (!success)
- docState.docWriter.setAborting();
- ....
- }else {//当前field需要分词
- final TokenStream stream;
- //确定field在创建的时候是否已经有了一个内容词语的tokenStream
- final TokenStream streamValue = field.tokenStreamValue();
- //field已经有分好词的tokenStream
- if (streamValue != null)
- stream = streamValue;
- else {//field没有分好词的tokenStream
- final Reader reader;
- //确定field的内容是否是Reader类型
- final Reader readerValue = field.readerValue();
- //field内容是Reader类型
- if (readerValue != null)
- reader = readerValue;
- else {
- //filed内容不是Reader类型,则判断是否是String
- String stringValue = field.stringValue();
- if (stringValue == null)
- throw new IllegalArgumentException("field must have either TokenStream, String or Reader value");
- perThread.stringReader.init(stringValue);
- reader = perThread.stringReader;
- }
- //用分析器处理当前field(进行分词和过滤),并加入到postingTable
- stream = docState.analyzer.reusableTokenStream(fieldInfo.name, reader);
- }
- ......第二部分源码.....
- }//end if(需要索引)
- consumer.finish();
- endConsumer.finish();
- }//end for(每一个field)
- }//end processFields
第一部分源码的主要作用就是根据每一个需要检索的field的不同操作方式进行处理。如果field不需要分词,则直接将filed交给TermsHashPerField建立索引结构(code line: 30, 32)。如果field需要分词,则首先判断field的value是不是Reader类型(分析器Analyzer只接受Reader类型数据),不是则将value字符串值包装成Reader类型(code line:57)。再让Analyzer分词得到TokenStream stream(code line : 61)。然后将stream中的每一个token交给TermsHashPerField建立索引结构(请看后面的第二部分代码)。
我们用上一节的doc1的例子来查看这个stream的结果,其中doc1通过上一节加工成了DocFieldProcessorPerThread fields[]数组。而fields[0]就是指doc1中名字为cotent的field集合,这个集合有两个content field。
content field 1: The lucene is a good IR. I hope I can lean.
stream 的结果显示(已经去停用词了):
token | type | offset | pos |
lucene | <ALPHANUM> | (4,10) | 2 |
good | <ALPHANUM> | (16,20) | 3 |
ir | <ALPHANUM> | (21,23) | 1 |
i | <ALPHANUM> | (25,26) | 1 |
hope | <ALPHANUM> | (27,31) | 1 |
i | <ALPHANUM> | (32,33) | 1 |
can | <ALPHANUM> | (34,37) | 1 |
lean | <ALPHANUM> | (38,42) | 1 |
content field 2: Lucene 3.0 like a teacher. I love it.
stream 的结果显示(已经去停用词了):
token | type | offset | pos |
lucene | <ALPHANUM> | (0,7) | 1 |
3.0 | <NUM> | (8,11) | 1 |
like | <ALPHANUM> | (12,16) | 1 |
teacher | <ALPHANUM> | (19,26) | 2 |
i | <ALPHANUM> | (28,29) | 1 |
love | <ALPHANUM> | (30,34) | 1 |
Part II src code:
- ...... 第一部分.....
- // 将TokenStream内部指针指向第一个token
- stream.reset();
- final int startLength = fieldState.length;
- try {
- //记录当前token首字母在文本中的位置,如果token是TokenStream中的第一个词语,则offsetEnd=-1
- int offsetEnd = fieldState.offset-1;
- //获取分词后tokenStream的每一个token的全部信息
- boolean hasMoreTokens = stream.incrementToken();
- fieldState.attributeSource = stream;
- //得到当前token的OffsetAttribute属性信息
- OffsetAttribute offsetAttribute =fieldState.attributeSource.addAttribute(OffsetAttribute.class);
- //得到当前token的PositionIncrementAttribute属性信息
- PositionIncrementAttribute posIncrAttribute =fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);
- //利用TermsHashPerField将每一个token加入倒排索引结构
- consumer.start(field);
- for(;;) {
- //tokenStream结束
- if (!hasMoreTokens) break;
- //得到当前token的positionIncreament属性
- final int posIncr = posIncrAttribute.getPositionIncrement();
- //此时fieldState.position表示当前token所在原文本中的词语位置,即token前面有多少个词语
- fieldState.position += posIncr;
- //positionIncreament属性计算的时候就是相隔的词语数量+1,因此统计当前token前面的词语数量的时候,要减1
- if (fieldState.position > 0) {
- fieldState.position--;
- }
- if (posIncr == 0)
- fieldState.numOverlap++;
- try {
- //利用TermsHashPerField将当前token以及fieldState当前所记录的位置信息一并加入进倒排索引结构中
- consumer.add();
- success = true;
- } finally {
- if (!success)
- docState.docWriter.setAborting();
- }
- //准备记录下一个token,因此将当前token算入进去
- fieldState.position++;
- //记录当前token的尾字母在原文本中所在的位置
- offsetEnd = fieldState.offset + offsetAttribute.endOffset();
- //fieldState.length记录了当前已经处理了的token数量,如果超过了允许的最大数量,则后面的词语将被丢弃,不再加入到索引中。
- if (++fieldState.length >= maxFieldLength) {
- if (docState.infoStream != null)
- docState.infoStream.println("maxFieldLength " +maxFieldLength+ " reached for field " + fieldInfo.name + ", ignoring following tokens");
- break;
- }
- //取下一个token
- hasMoreTokens = stream.incrementToken();
- }
- stream.end();
- } finally {
- stream.close();
- }
第二部分源码的主要作用就是循环得到stream(第一部分代码)中的每一个token(code line: 21),计算token在原始文本中的位置(code line: 28,31),并保存在fieldState.position和fieldState.offset中。同时token和fieldState中的统计信息交给TermsHashPerField建立倒排索引结构(code line: 38)。
总结 ,下图 展示了 DocInverterPerField 的作用。它会把不需要分词的field以红色方框的结构(field value)传给TermsHashPerField和 FreqProxTermsWriterPerField 来建立索引。而把需要分词的content field变成一个个蓝色方框的结构(token && position)来建立索引,接下来就是对token建立倒排索引的过程了。请参见《索引创建(4):DocumentWriter 处理流程三 》。
注意,上图蓝色方框的箭头并不是指DocInverterPerField会把他们建立成链表结构。事实上,这些箭头只是为了表明一个个token依次被 TermsHashPerField加入索引结构的。另外,相同名字的field中的词语会依次处理,就如同上面fields[0]和fields[1]。