您的位置:首页 > 其它

Lucene 3.5最新版 在2011-11-26日发布了

2013-02-06 18:18 155 查看
Lucene进行了大量优化、改进和Bug的修复,包括

1.        大大降低了控制开放的IndexReader上的协议索引的RAM占用(3~5倍)。

2.        新增IndexSearcher.searchAfter,可在指定ScoreDoc后返回结果(例如之前页面的最后一个文档),以支持deep页用例。

3.        新增SearcherManager,以管理共享和重新开始跨多个搜索线程的IndexSearchers。基本的IndexReader实例如果不再进行引用,则会被安全关闭。

4.        新增SearcherLifetimeManager,为跨多个请求(例如:paging/drilldown)的索引安全地提供了一个一致的视图。

5.        将IndexWriter.optimize重命名为forceMerge,以便去阻止使用这种方法,因为它的使用代价较高,且也不需要使用。

6.        新增NGramPhraseQuery,当使用n-gram分析时,可提升30%-50%的短语查询速度。

7.        重新开放了一个API(IndexReader.openIfChanged),如果索引没有变化,则返回空值,而不是旧的reader。

8.        Vector改进:支持更多查询,如通配符和用于产生摘要的边界分析。

9.        修复了若干Bug。

针对做出一个简单的搜索引擎,笔者针对遇到的问题进行探讨:

1.        关于查询关键字的问题:
StringqueryStr =”中国”;
QueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_35, fields,
luceneAnalyzer);       
Query query = queryParser.parse(queryString);
Lucene对这个查询是不分大小写的,当搜索关键字为英文加数字或汉字或其他字符的时候,例如:“swing12”、“swing我sd”等,Lucene会先对这个关键字进行分词,即分成英文+数字或汉字的形式,然后去索引,这样docment中含有”swing”和”12”的Field都被索引出来了。可以达到模糊查询,若想要精确查询请往下看。
2.   针对Lucene在显示查询结果时,通过高亮显示功能把doc.get( "Content ")中的内容显示字符不带标点的问题。
因为lucene在做索引的时候是要先切分的,你如果事先切分的时候就去掉了标点符号,那么你搜索出结果就不会有标点了。
所以当你使用lucene自带的分析器的时候要注意,笔者使用的是IK分词即可解决这一问题。
3.   针对Lucene搜素时查询出结果的显示个数问题。
创建结果文档收集器:
public static TopScoreDocCollector create(intnumHits, boolean docsScoredInOrder);
意思是其根据是否按照文档号从小到大返回文档而创建,false不按照文档号从小到大。numHits返回定义要取出的文档数。
而搜集文档号函数:
public void score(Collector collector);
当创建完毕Scorer对象树和SumScorer对象树后,用:

scorer.score(collector) ; 其不断的得到合并的倒排表后的文档号,并收集它们。
例:
public
static void search() throwsException{       

      String queryString = "test1";     

      String[] fields = {"id","content"};       

      QueryParser queryParser =
new
MultiFieldQueryParser(Version.LUCENE_35,fields, luceneAnalyzer);       

      Query query = queryParser.parse(queryString);       

      IndexReader reader = IndexReader.open(FSDirectory.open(newFile(indexpath)));       

      //IndexReader reader = IndexReader.open(indexDir);       

      IndexSearcher searcher =
new
IndexSearcher(reader);               
      TopScoreDocCollector results = TopScoreDocCollector.create(10,
false);//判断是否按照从小到大顺序排列文档号,并取出10条记录(随即取);如果为True,则取前10条。
      Date dt1 = new Date();
      System.out.println("开始查询时间:"+dt1.getTime());
      System.out.println("查询关键字 : "+ queryString);
      searcher.search(query, results);               

      Date dt2 = new Date();
      System.out.println("结束查询时间:"+dt2.getTime());
      System.out.println();
      System.out.println("查询耗时:" +(dt2.getTime()-dt1.getTime()) + "ms");
      TopDocs topDocs = results.topDocs(0, 10); //查询前10条放入结果集。   

      System.out.println("命中数: " + topDocs.totalHits);   

      for(int j=0 ; j<topDocs.scoreDocs.length; j++) {
          ScoreDoc scoreDoc = topDocs.scoreDocs[j];
          Document doc =searcher.doc(scoreDoc.doc);           

          System.out.println("第 "+ doc.get("id")+"
行");           

          System.out.println("内容: " +doc.get("content"));
      }          
      }
   }
由代码我们可以知道:
collector的作用就是首先计算文档的打分,然后根据打分,将文档放入优先级队列(最小堆)中,最后在优先级队列中取前N篇文档。
然而存在一个问题,如果要取10篇文档,而第8,9,10,11,12篇文档的打分都相同,则抛弃那些?Lucene的策略是,在文档打分相同的情况下,文档号小的优先。
也即8,9,10被保留,11,12被抛弃。
由上面的叙述可知,创建collector的时候,根据文档是否将按照文档号从小到大的顺序返回而创建InOrderTopScoreDocCollector或者OutOfOrderTopScoreDocCollector。
对于InOrderTopScoreDocCollector,由于文档是按照顺序返回的,后来的文档号肯定大于前面的文档号,因而当score<= pqTop.score的时候,直接抛弃。
对于OutOfOrderTopScoreDocCollector,由于文档不是按顺序返回的,因而当score<pqTop.score,自然直接抛弃,当score==pqTop.score的时候,则要比较后来的文档和前面的文档的大小,如果大于,则抛弃,如果小于则入队列。
4.    Lucene的打分机制:
BooleanScorer2的打分函数如下:
将子语句的打分乘以coord(一个文章中包含的关键字越多则打分越高)

public float score() throws IOException {
  coordinator.nrMatchers = 0;
  float sum = countingSumScorer.score();//当前文档的得分
  return sum * coordinator.coordFactors[coordinator.nrMatchers];//coord

}
ConjunctionScorer的打分函数如下:
将取交集的子语句的打分相加,然后乘以coord
public float score() throws IOException {
  float sum = 0.0f;
  for (int i = 0; i < scorers.length; i++) {
    sum += scorers[i].score();
  }
  return sum * coord;
}
DisjunctionSumScorer的打分函数如下:
public float score() throws IOException { return currentScore; }
currentScore计算如下:
currentScore += scorerDocQueue.topScore();
以上计算是在DisjunctionSumScorer的倒排表合并算法中进行的,其是取堆顶的打分函数。
public final float topScore() throws IOException {
    return topHSD.scorer.score();
}
ReqExclScorer的打分函数如下:
仅仅取required语句的打分
public float score() throws IOException {
  return reqScorer.score();
}
ReqOptSumScorer的打分函数如下:
上面曾经指出,ReqOptSumScorer的nextDoc()函数仅仅返回required语句的文档号。 而optional的部分仅仅在打分的时候有所体现,从下面的实现可以看出optional的语句的分数加到required语句的分数上,也即文档还是required语句包含的文档,只不过是当此文档能够满足optional的语句的时候,打分得到增加。

public float score() throws IOException {
  int curDoc = reqScorer.docID();
  float reqScore = reqScorer.score();
  if (optScorer == null) {
    return reqScore;
  }
  int optScorerDoc = optScorer.docID();
  if (optScorerDoc < curDoc && (optScorerDoc = optScorer.advance(curDoc)) == NO_MORE_DOCS) {
    optScorer = null;
    return reqScore;
  }
  return optScorerDoc == curDoc ? reqScore + optScorer.score() : reqScore;
}
TermScorer的打分函数如下:
整个Scorer及SumScorer对象树的打分计算,最终都会源自叶子节点TermScorer上。 从TermScorer的计算可以看出,它计算出tf *norm * weightValue =
tf * norm * queryNorm * idf^2 * t.getBoost()
public float score() {
  int f = freqs[pointer];
  float raw = f < SCORE_CACHE_SIZE ? scoreCache[f] : getSimilarity().tf(f)*weightValue;       

  return norms == null ? raw : raw * SIM_NORM_DECODER[norms[doc] & 0xFF];

}
Lucene的打分公式整体如下,2.4.1计算了图中的红色的部分,此步计算了蓝色的部分:

Coord(q,d)因子
d 代表docment中的filed个数,q为查询匹配的个数。
Coord(q,d) = q/d 即为一个docment中关键字多少的得分。
 
queryNorm(q)是查询权重对得分的影响。
queryNorm(q) = queryNorm(sumOfSquaredWeights)=1/(sumOfSquaredWeights^(1/2))  
sumOfSquaredWeights= q.getBoost()^2·∑( idf(t)·t.getBoost() )^2
t即为term,t in q  即为在查询中出现的term。
q.getBoost()是一个查询子句被赋予的boost值,因为Lucene中任何一个Query对象是可以通过setBoost(boost)方法设置一个boost值的。例如:
BooleanQuery bq1 = new BooleanQuery(); // 第一个BooleanQuery查询子句   
TermQuery tq1 = new TermQuery(new Term("title", "search"));  
tq1.setBoost(2.0f);  
bq1.add(tq1, Occur.MUST);   
TermQuery tq2 = new TermQuery(new Term("content", "lucene"));  
tq2.setBoost(5.0f);  
bq1.add(tq2, Occur.MUST);  
bq1.setBoost(0.1f); 
// 给第一个查询子句乘上0.1,实际是减弱了其贡献得分的重要性   
BooleanQuery bq2 = new BooleanQuery(); // 第二个BooleanQuery查询子句   
TermQuery tq3 = new TermQuery(new Term("title", "book"));  
tq3.setBoost(8.0f);  
bq2.add(tq3, Occur.MUST);  
TermQuery tq4 = new TermQuery(new Term("content", "lucene"));  
tq4.setBoost(5.0f);  
bq2.add(tq4, Occur.MUST);  
bq2.setBoost(10.0f); // 给第二个查询子句乘上10.0,该子句更重要   
BooleanQuery bq = new BooleanQuery(); 
// 对上述两个BooleanQuery查询子句再进行OR运算
  
bq.add(bq1, Occur.SHOULD);  
bq.add(bq2, Occur.SHOULD);  
例子代码意思::“我想要查询包含Lucene的文章,但标题最好是含有book的”,也就是说“我想查找介绍Lucene的书籍,如果没有没有关于Lucene的书籍,包含介绍Lucene查询search的文章也可以”。

所以上述两个布尔查询子句设置的boost值(0.1<<10.0),就对应于我们上述公式中的q.getBoost()。

 
idf(t)就是反转文档频率,含义是如果文档中出现Term的频率越高显得文档越不重要,Lucene中计算该值的公式如下:
idf(t) = 1.0 + log(numDocs/(docFreq+1))  
其中,numDocs表示索引中文档的总数,docFreq表示查询中Term在多个文档中出现。

t.getBoost()表示查询中的Term给予的boost值,例如上面代码中:
TermQuery tq3 = new TermQuery(new Term("title", "book"));  
tq3.setBoost(8.0f);  
title中包含book的Term,对匹配上的文档,通过上面公式计算,乘上t.getBoost()的值。
 
∑( tf(t in d)·idf(t)^2·t.getBoost()·norm(t,d) )因子
上面t还是在q中出现的Term即t in q。
norm(t,d)的含义,计算公式如下所示:
norm(t,d) = doc.getBoost()· lengthNorm· ∏ f.getBoost()  
norm(t,d)是在索引时(index-time)进行计算并存储的,在查询时(search-time)是无法再改变的,除非再重建索引。另外,Lucene在索引时存储norm值,而且是被压缩存储的,在查询时取出该值进行文档相关度计算,即文档得分计算。
需要注意的是,norm在进行codec的过程中,是有精度损失的,即不能保证decode(encode(x)) = x永远成立,例如 decode(encode(0.89)) = 0.75。
如果你在相关度调优过程中,发现norm的值破坏了文档相关性,严重的话,可以通过Field.setOmitNorms(true)方法来禁用norm,同时减少了该norm的存储开销,在一定程度上加快了查询过程中文档得分的计算。是否使用norm,需要根据你的应用来决定,例如,如果一个Field只存储一个Term,或者Field很短(包含的Term很少),一般是不需要存储norm的。
doc.getBoost()
这个就是Document的boost值,在索引的时候可以通过setBoost(boost)方法设置,例如我们一般认为title会比content更重要,所以在索引时可以对title进行boost(大于1.0)。
lengthNorm是一个与Field长度(包含Term数量)有关的因子,Lucene中计算公式如下:
lengthNorm = 1.0 / Math.sqrt(numTerms);
其中,numTerms表示一个Field中Term的数量。
一般来说,一个Term在越短的Field中出现,表示该Term更重要,有点类似idf的含义。

∏ f.getBoost()

Lucene索引时,一个Document实例中,可以多次添加具有同一个Field名称的Field对象,但是值不相同,如下代码:

Document doc = new Document();  
doc.add(new Field("title", "search engine", Field.Store.YES, Field.Index.ANALYZED));  
Field fcontent1 = new Field("content", "nutch solr lucene lucene search server", Field.Store.YES, Field.Index.ANALYZED);  
fcontent1.setBoost(2.0f);  
doc.add(fcontent1);  
Field fcontent2 = new Field("content", "good lucene luke lucene index server", Field.Store.YES, Field.Index.ANALYZED);  
fcontent2.setBoost(5.0f);  
doc.add(fcontent2);  
indexWriter.addDocument(doc);  
我们在doc里面添加了同名content的两个字符串,对与这种情况,在计算得分的时候,是通过 ∏ f.getBoost()连乘积来计算得到的。
例如,我们查询content:lucene,上面Document doc中两个content的Field都匹配上了,在计算的时候有: ∏ f.getBoost() = 2.0 * 5.0 = 10.0。如果查询content:solr,则只有一个Field匹配上了,则 ∏ f.getBoost()=2.0。

 
打分计算到此结束。
 
5.        关于Lucene常用查询对象
 
Query:
BooleanQuery()  //布尔查询
TermQuery()   //原子查询,完全匹配某个词条
WildcardQuery() //通配符查询
MultiFieldQueryParser()  //单字段多值查询
例如:Filed1:水果,content:苹果,香蕉;Filed2:水果,content:梨,猕猴桃,草莓
RangeQuery()   //指定范围查询
FuzzyQuery()   //英文通配符,中文不适用
PrefixQuery()  //以XXX开头的查询,例如查询以“今天”开头的新闻
PhraseQuery()    //不严格匹配查询
BoostingQuery即BooleanQuery()
BoostingQuery包含三个成员变量:
 
•Query match:这是结果集必须满足的查询对象//即是否是模糊查询或者完全匹配查询(MUST
完全匹配,Like模糊)
•Query context:此查询对象不对结果集产生任何影响,仅在当文档包含context查询的时候,将文档打分乘上boost//一般设置为0即无影响文档打分。
•float boost
在BoostingQuery构造函数中:
 
public BoostingQuery(Query match, Query context, float boost) {
      this.match = match;
      this.context =(Query)context.clone();
      this.boost = boost;
      this.context.setBoost(0.0f);
在BoostingQuery的rewrite函数如下:
public Query rewrite(IndexReader reader) throws IOException {
  BooleanQuery result = newBooleanQuery() {
    @Override
    public Similarity getSimilarity(Searchersearcher) {
      return new DefaultSimilarity(){
        @Override
        public float coord(intoverlap, int max) {
          switch (overlap) {
          case 1:
            return 1.0f;
          case 2:
            return boost;
          default:
            return 0.0f;
          }
        }
      };
    }
  };
  result.add(match,BooleanClause.Occur.MUST);
  result.add(context,BooleanClause.Occur.SHOULD);
  return result;
}
 
 
由上面实现可知,BoostingQuery最终生成一个BooleanQuery,第一项是match查询,是MUST,即required,第二项是context查询,是SHOULD,即optional
然而由查询过程分析可得,即便是optional的查询,也会影响整个打分。
所以在BoostingQuery的构造函数中,设定context查询的boost为零,则无论文档是否包含context查询,都不会影响最后的打分。
在rewrite函数中,重载了DefaultSimilarity的coord函数,当仅包含match查询的时候,其返回1,当既包含match查询,又包含context查询的时候,返回boost,也即会在最后的打分中乘上boost的值。
下面我们做实验如下:
索引如下文件:
 
file01: apple other other other boy

 
file02: apple apple other other other
 
file03: apple apple apple other other
 
file04: apple apple apple apple other
 
对于如下查询(1):
 
TermQuery must = new TermQuery(newTerm("contents","apple"));

TermQuery context = new TermQuery(newTerm("contents","boy"));

BoostingQuery query = new BoostingQuery(must, context, 1f);
 
或者如下查询(2):
 
TermQuery query = new TermQuery(newTerm("contents","apple"));
 
两者的结果是一样的,如下:
 
docid : 3 score : 0.67974937
docid : 2 score : 0.58868027
docid : 1 score : 0.4806554
docid : 0 score : 0.33987468
 
自然是包含apple越多的文档打分越高。
 
然而他们的打分计算过程却不同,用explain得到查询(1)打分细节如下:
 
docid : 0 score : 0.33987468
0.33987468 = (MATCH) fieldWeight(contents:apple in 0), product of:

  1.0 = tf(termFreq(contents:apple)=1)
  0.7768564 = idf(docFreq=4,maxDocs=4)

  0.4375 = fieldNorm(field=contents,doc=0)
 
explain得到的查询(2)的打分细节如下:
 
docid : 0 score : 0.33987468
0.33987468 = (MATCH) sum of:
  0.33987468 = (MATCH)fieldWeight(contents:apple in 0), product of:

    1.0 =tf(termFreq(contents:apple)=1)

    0.7768564 = idf(docFreq=4,maxDocs=4)

    0.4375 =fieldNorm(field=contents, doc=0)

  0.0 = (MATCH)weight(contents:boy^0.0 in 0), product of:

    0.0 =queryWeight(contents:boy^0.0), product of:

      0.0 = boost
      1.6931472 = idf(docFreq=1,maxDocs=4)

      1.2872392 = queryNorm
    0.74075186 = (MATCH)fieldWeight(contents:boy in 0), product of:

      1.0 =tf(termFreq(contents:boy)=1)

      1.6931472 = idf(docFreq=1,maxDocs=4)

      0.4375 =fieldNorm(field=contents, doc=0)
 
可以知道,查询(2)中,boy的部分是计算了的,但是由于boost为0被忽略了。
 
让我们改变boost,将包含boy的文档打分乘以10:
 
TermQuery must = new TermQuery(newTerm("contents","apple"));

TermQuery context = new TermQuery(newTerm("contents","boy"));

BoostingQuery query = new BoostingQuery(must, context, 10f);
 
结果如下:
 
docid : 0 score : 3.398747
docid : 3 score : 0.67974937
docid : 2 score : 0.58868027
docid : 1 score : 0.4806554
 
explain得到的打分细节如下:
 
docid : 0 score : 3.398747
3.398747 = (MATCH) product of:

  0.33987468 = (MATCH) sum of:

    0.33987468 = (MATCH)fieldWeight(contents:apple in 0), product of:

      1.0 =tf(termFreq(contents:apple)=1)

      0.7768564 = idf(docFreq=4,maxDocs=4)

      0.4375 =fieldNorm(field=contents, doc=0)

    0.0 = (MATCH)weight(contents:boy^0.0 in 0), product of:

      0.0 =queryWeight(contents:boy^0.0), product of:

        0.0 = boost
        1.6931472 = idf(docFreq=1,maxDocs=4)

        1.2872392 = queryNorm

      0.74075186 = (MATCH)fieldWeight(contents:boy in 0), product of:

        1.0 =tf(termFreq(contents:boy)=1)

        1.6931472 = idf(docFreq=1,maxDocs=4)

        0.4375 =fieldNorm(field=contents, doc=0)

  10.0 = coord(2/2)
 
6.        关于Lucene索引文件分析
首先看下索引文件类型:

i.         .fnm文件//片段名
当向一个IndexWriter索引器实例添加Document的时候,调用了IndexWriter的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 instanceofFSDirectory){
   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 finalbyte STORE_PAYLOADS = 0x20;
ii.       _N.fdt文件内容:
(1) Document中需要存储的Field的数量;
(2) 每个Field在Document中的编号;
(3) 每个Field关于是否分词、是否压缩、是否以二进制存储这三个指标的一个组合值;
(4) 每个Field的长度;
(5)每个Field的内容(binaryValue或stringValue);
_N.fdx文件内容:
(1)一个Field在_N.fdt文件中位置
代码分析:
接着,在DocumentWriter类中的addDocumet()方法中,根据Directory实例、segment的名称、一个FieldInfos的实例构造了一个FieldsWriter类的实例:
FieldsWriter fieldsWriter =  new FieldsWriter(directory, segment, fieldInfos);
可以从FieldsWriter类的构造方法可以看出,实际上,根据生成的segment的名称(_ram_N和_N)创建了两个输出流对象:
 
    FieldsWriter(Directoryd, 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 voidaddDocument(Document doc) throws IOException {
       indexStream.writeLong(fieldsStream.getFilePointer());    // 向indexStream中(即_N.fdx文件)中写入fieldsStream(_N.fdt文件)流中的当前位置,也就是写入这个Field信息的位置
 
        int storedCount =0;
        IteratorfieldIterator = doc.getFields().iterator();
        while (fieldIterator.hasNext()){   // 循环遍历该Document中所有Field,统计需要存储的Field的个数
            Fieldablefield = (Fieldable) fieldIterator.next();
            if(field.isStored())
               storedCount++;
        }
      fieldsStream.writeVInt(storedCount);   // 存储Document中需要存储的的Field的个数,写入到_N.fdt文件
 
        fieldIterator =doc.getFields().iterator();
        while(fieldIterator.hasNext()) {
            Fieldablefield = (Fieldable) fieldIterator.next();
            // if thefield as an instanceof FieldsReader.FieldForMerge, we're in merge mode
            // andfield.binaryValue() already returns the compressed value for a field
            // withisCompressed()==true, so we disable compression in that case
            booleandisableCompression = (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"));
                      }
                  }
                  finalint 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();
                    finalint 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);
 
iii.     _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) {   // 其中,intlastPayloadLength = -1;
             // the lengthof the current payload equals the length
            // of theprevious one. So we do not have to store the length
            // again andwe only shift the position delta by one bit
             prox.writeVInt(delta * 2);    //其中,int delta = position - lastPosition,int position = positions[j];
            } else {
            // the lengthof the current payload is different from the
            // previousone. We shift the position delta, set the lowest
            // bit andstore the current payload length as VInt.
             prox.writeVInt(delta* 2 + 1);
             prox.writeVInt(payloadLength);
             lastPayloadLength = payloadLength;
            }
            if(payloadLength > 0) {
            // writecurrent payload
             prox.writeBytes(payload.data, payload.offset, payload.length);
            }
          } else {
          // field doesnot 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
* similarity.lengthNorm(fi.name, fieldLengths
);
        IndexOutput norms= directory.createOutput(segment + ".f" + n);
        try {
         norms.writeByte(Similarity.encodeNorm(norm));
        } finally {
          norms.close();
        }
      }
    }
}
 
将一些标准化因子的信息,都写入到了_N.nrm文件。其中每个segment对应着一个_N.nrm文件
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: