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

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.        重新开放了一个APIIndexReader.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()    //不严格匹配查询

BoostingQueryBooleanQuery()

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);

BoostingQueryrewrite函数如下:

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的部分是计算了的,但是由于boost0被忽略了。

 

让我们改变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的时候,调用了IndexWriteraddDocument()方法,在方法的内部调用如下:

buildSingleDocSegment() —> String segmentName =newRamSegmentName();

这时,调用newRamSegmentName()方法生成了一个segment的名称,形如_ram_N,这里N36进制数。

这个新生成的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) 每个FieldDocument中的编号;

(3) 每个Field关于是否分词、是否压缩、是否以二进制存储这三个指标的一个组合值;

(4) 每个Field的长度;

(5)每个Field的内容(binaryValuestringValue)

_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[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文件

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值