Lucene通过计算文档的得分来确定查询结果文档的相似度。如果你希望通过干预Lucene查询来改变查询结果的排序,你就需要对Lucene的得分计算有所理解。Lucene得分计算公式如下所示:
score(q,d) = coord(q,d)·queryNorm(q)·∑( tf(t in d)·idf(t)^2·t.getBoost()·norm(t,d) )
其中,t in q。
下面详细解释公式乘积的每个因子的含义,以及是如何计算的,这样能够加深对Lucene得分计算的理解,才能在实际应用中根据需要调整各个参数,从而制定满足应用的排序策略。
coord(q,d)因子
coord(q,d)能够影响到检索结果文档的得分,它的计算公式如下:
coord(q,d) = overlap / maxOverlap
例如,查询关键词中经过解析(QueryParser)处理,得到两个Term分别为title:search和content:lucene,那么对应于公式中有,maxOverlap=2。
例如,对于下面这个索引的Document,代码如下:
Document doc = new Document();我们看到,检索title:search和content:lucene这两个Term,在title这个Field中( "search engine")匹配上title:search,
doc.add(new Field("title", "search engine", Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("content", "good lucene luke lucene search server", Field.Store.YES, Field.Index.ANALYZED));
indexWriter.addDocument(doc);
在content这个Field中("good lucene luke lucene search server")匹配上了content:lucene,所以对应于公式中有,overlap=2,所以coord(q,d)=2/2=1。
queryNorm(q)因子
queryNorm(q)是查询权重对得分的影响,它的计算公式如下:
queryNorm(q) = queryNorm(sumOfSquaredWeights)=1/(sumOfSquaredWeights^(1/2))
sumOfSquaredWeights
继续看一下,在BooleanQuery中sumOfSquaredWeights的计算:
sumOfSquaredWeights = q.getBoost()^2·∑( idf(t)·t.getBoost() )^2
因为这是在计算查询的权重,所以上式求和部分中出现的t都是在q里面出现的Term(t in q)。
q.getBoost()
上式中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)
idf(t)就是反转文档频率,含义是如果文档中出现Term的频率越高显得文档越不重要,Lucene中计算该值的公式如下:
idf(t) = 1.0 + log(numDocs/(docFreq+1))
其中,numDocs表示索引中文档的总数,docFreq表示查询中Term在多个文档中出现。
t.getBoost()
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)的含义,计算公式如下所示:
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
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。
参考内容
http://lucene.apache.org/java/3_1_0/api/core/org/apache/lucene/search/Similarity.html