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(一个文章中包含的关键字越多则打分越高)
ConjunctionScorer的打分函数如下:
将取交集的子语句的打分相加,然后乘以coord
DisjunctionSumScorer的打分函数如下:
ReqExclScorer的打分函数如下:
仅仅取required语句的打分
ReqOptSumScorer的打分函数如下:
上面曾经指出,ReqOptSumScorer的nextDoc()函数仅仅返回required语句的文档号。 而optional的部分仅仅在打分的时候有所体现,从下面的实现可以看出optional的语句的分数加到required语句的分数上,也即文档还是required语句包含的文档,只不过是当此文档能够满足optional的语句的时候,打分得到增加。
TermScorer的打分函数如下:
整个Scorer及SumScorer对象树的打分计算,最终都会源自叶子节点TermScorer上。 从TermScorer的计算可以看出,它计算出tf *norm * weightValue =
tf * norm * queryNorm * idf^2 * t.getBoost()
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的含义。
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文件
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 } |
将取交集的子语句的打分相加,然后乘以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; } |
public float score() throws IOException { return currentScore; } currentScore计算如下: currentScore += scorerDocQueue.topScore(); 以上计算是在DisjunctionSumScorer的倒排表合并算法中进行的,其是取堆顶的打分函数。 public final float topScore() throws IOException { return topHSD.scorer.score(); } |
仅仅取required语句的打分
public float score() throws IOException { return reqScorer.score(); } |
上面曾经指出,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; } |
整个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]; } |
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文件
相关文章推荐
- Eclipse 3.5发布
- ASP.NET 3.5 Extensions CTP预览版发布了
- JavaEE快速开发框架Wabacus 3.5发布,开发效率提高5倍以上
- 微软发布 Windows SDK for Windows Server 2008 & .NET Framework 3.5
- VS 2008 和 .NET 3.5 Beta 2 发布了
- Linux Kernel 3.5发布:网络、电源管理和安全性能提升
- 学习Lucene过程中遇到的错误(Lucene 3.5)
- 14、学习Lucene3.5索引之同义词分词器设计思路
- 微软MSN宣布手机MSN 3.5版正式发布
- Lucene-4.0.0的demo编译、发布和使用
- Linux 内核发布 3.5 正式版
- VS 2008 和 .NET 3.5 Beta 2 发布了
- 发布一个最新版Productivity Power Tools中的CopyAsHtml扩展的修改版(含工具和源码)
- 转:甲骨文发布大数据解决方案 含最新版NoSQL数据库
- Lucene的一个简单的标准测试(Lucene包基于3.5版本的)
- DotNetTextBoxV6.0在线编辑器控件(For Asp.net2.0/3.0/3.5)免费版发布!
- ASP.NET 3.5技术专题发布
- 开源内容管理系统Joomla3.5发布 基于PHP 7
- lucene3.5建立索引和查询
- vs2008 目标框架 发布遇到的问题. (总是必须安装3.5框架的解决办法)