Lucene Scoring 评分机制

来源:互联网

Lucene 评分体系/机制(lucene scoring)是 Lucene 出名的一核心部分。它对用户来说隐藏了很多复杂的细节,致使用户可以简单地使用 lucene。但个人觉得:如果要根据自己的应用调节评分(或结构排序),十分有必须深入了解 lucene 的评分机制。

Lucene scoring 组合使用了 信息检索的向量空间模型 和 布尔模型 。

首先来看下 lucene 的评分公式(在 Similarity 类里的说明)

score(q,d)   =   coord(q,d) ·  queryNorm(q) ·

( tf(t in d) ·  idf(t)2 ·  t.getBoost() ·  norm(t,d) )

t in q

其中:

  1. tf(t in d) 关联到项频率,项频率是指 项 t 在 文档 d 中出现的次数 frequency。默认的实现是:tf(t in d) =
    frequency½
  2. idf(t) 关联到反转文档频率,文档频率指出现 项 t 的文档数 docFreq。docFreq 越少 idf 就越高(物以稀为贵),但在同一个查询下些值是相同的。默认实现:idf(t) =
    1 + log ( 

    numDocs

    –––––––––

    docFreq+1

    )

  3. coord(q,d) 评分因子,是基于文档中出现查询项的个数。越多的查询项在一个文档中,说明些文档的匹配程序越高。默认是出现查询项的百分比。
  4. queryNorm(q)查询的标准查询,使不同查询之间可以比较。此因子不影响文档的排序,因为所有有文档都会使用此因子。默认值:queryNorm(q)   =   queryNorm(sumOfSquaredWeights) =1

    ––––––––––––––

    sumOfSquaredWeights½

     

     

    每个查询项权重的平分方和(sumOfSquaredWeights)由 Weight 类完成。例如 BooleanQuery 地计算:

    sumOfSquaredWeights =   q.getBoost() 2 ·

    ( idf(t) ·  t.getBoost() ) 2

    t in q

  5. t.getBoost()查询时期的 项 t 加权(如:java^1.2),或者由程序使用 setBoost()。
  6. norm(t,d)压缩几个索引期间的加权和长度因子:
    • Document boost - 文档加权,在索引之前使用 doc.setBoost()
    • Field boost - 字段加权,也在索引之前调用 field.setBoost()
    • lengthNorm(field) - 由字段内的 Token 的个数来计算此值,字段越短,评分越高,在做索引的时候由 Similarity.lengthNorm 计算。

    以上所有因子相乘得出 norm 值,如果文档中有相同的字段,它们的加权也会相乘:

    norm(t,d)   =   doc.getBoost() ·  lengthNorm(field) ·

    f.getBoost()

    field f in d named as t

    索引的时候,把 norm 值压缩(encode)成一个 byte 保存在索引中。搜索的时候再把索引中 norm 值解压(decode)成一个 float 值,这个 encode/decode 由 Similarity 提供。官方说:这个过程由于精度问题,以至不是可逆的,如:decode(encode(0.89)) = 0.75。

计算这个评分涉及到几个核心的类/接口:Similarity、Query、Weight、Scorer、Searcher,由它们或其子类来完成评分的计算。先来看下它们的类图:

lucene search score uml

lucene search score uml, 点击放大

搜索中,评分的过程:

  1. 创建一个查询对象 Query,传给 Searcher,具体来讲可能是 IndexSearcher。
  2. Searcher 根据 Query 创建一个对应的 Weight(是 Query 的内部特征表示),接着 Weight 会创建对应的 Scorer。
  3. Searcher 会创建 Hitcollector 并传到 Scorer,scorer 找到匹配的文档并计算评分,最后写到 Hitcollector 中。

Query、Weight、Scorer 三都关系十分密切,尤其是 Query 和 Weight。Weight 是计算查询权重和创建 Scorer 的。Query 为了可以重用把内部的特征抽象为 Weight,由子类去完成一些相关评分的计算。

任何 Searcher 依赖的状态都存储在 Weight 实现中,而不是在Query 中,这样可以重用 Query。

Weight 的生命周期(被使用):

  1. Weight 由顶层的 Query 创建。Query.createWeight(Searcher),创建的 Weight 给 Searcher 去使用。
  2. 当用 Similarity.queryNorm(float) 来计算查询标准化因子(query normalization)的时候,Weight.sumOfSquaredWeights() 会被调用。
  3. 查询标准化因子(query normalization)会传给 Weight.normalize(float)计算,这个时候权重(weighting)计算完成。
  4. 创建一个 Scorer。

自定义评分的计算

可以实现一个 Similarity 换掉默认的。它仅限于 Scorer、Weight 计算好的因子值再加工。要想对评分有更强的控制力,可以实现一套 Query、Weight、Scorer。

  • Query 是用户信息需要的抽象
  • Weight 是 Query 的内部特性表示的抽象
  • Scorer 抽象公用的计算评分功能,提供计算评分和解说(explanation)评分的能力。

Query 子类实现的方法:

  1. createWeight(Searcher searcher) -- Weight 是 Query 内部代表,所以每个 Query 都必实现一个 Weight,此方法就是生成一个Query对应的Weight对象。
  2. rewrite(IndexReader reader) -- 重写查询为原始的查询,原始的查询有:TermQuery,BooleanQuery……

Weight 接口方法:

  1. Weight#getQuery() -- 指出代表 Weight 的 Query。
  2. Weight#getValue() -- Query 的权重,例如:TermQuery.TermWeight 的 value = idf^2 * boost * queryNorm
  3. Weight#sumOfSquaredWeights() -- 各查询项的平方和,如,TermWeight 的 = (idf * boost)^2
  4. Weight#normalize(float) -- 决定查询标准化的因子,查询标准化值可以在不同 Query 比较 score
  5. Weight#scorer(IndexReader) -- 创建 Query 对应的评分器 Scorer,它的责任是给 Query 匹配到的文档评分。
  6. Weight#explain(IndexReader, int) -- 给指定的文档详细解说评分值是怎么得来了。

Scorer 子类实现的方法:

  1. Scorer#next() -- 预取匹配到的下一文档,有才返回 true。
  2. Scorer#doc() -- 返回当前匹配到的文档id,它必须 next() 调用后才有效。
  3. Scorer#score() -- 返回当前文档的评分,此值可以由应用程序以任何适当的方式给出,如 TermScorer 返回 tf * Weight.getValue() * fieldNorm
  4. Scorer#skipTo(int) -- 跳到大于或等于 int 的匹配文档上。很多情况下,在结果集中 skipTo 比较循环更加快速高效。
  5. Scorer#explain(int) -- 给出评分产生的细节。

要实现一套 Query、Weight、Scorer,最好还是看下 TermQuery、TermWeight、TermScorer。

当 Lucene 中没有想要的查询时(包括不同的评分细节),自定义Query 可能帮得上忙。

 

11.3.2  Lucene评分算法

那么,Lucene中是如何确定各个Document评分的呢?下面将详细介绍该功能的基本原理。

文档的得分是在用户进行检索时实时计算出来的。如果在建立索引时就已经将每个文档的得分计算好,那么当用户输入任何关键字时,得分最高的文档都会被排在返回结果的最前面,这显然是不合理的。

因此,所有文档的得分应当都与用户输入的关键字有关系,而且是实时运算的结果。其实,所谓得分,可以简单理解成是某个关键字在某文档中出现的频率。

图11-6所示的公式就是Lucene用于计算某个关键字在对应于某文档的得分。

图11-6  Lucene的得分公式

在Lucene得分公式中,已经包含了影响文档评分的各种因素。在表11.1中详细介绍了每一种因素对搜索结果评分的影响作用。

表11-1                                                 Lucene得分公式的解释

因 素

在公式中的作用描述

tf(t in d)

词条t在文档d中出现的词频

idf( t )

词条t在文档中的倒排词频

boost(t.field in d)

在索引过程中设置的字段参数

lengthNorm(t.field in d)

字段的标准化值,表明在字段中存储了多少词条,这个数值是在索引过程中计算出来的,并且也存储在索引中

coord(q, d)

协调因子,它的计算是基于文档d中所包含的所有可供查询的词条数量

queryNorm(q)

在给出每个查询条目的方差和后,计算某查询的标准化值

 

11.3.3  改变文档的得分

除了内置的得分算法外,Lucene还提供了一种方法来改变每个文档的得分。

在代码11.3中,初始化Document后,使用了Document的setBoost方法来改变一下文档的boost因子。这种做法的实际目的是将文档的得分乘以这个因子,以这个新的数作为文档的得分。

代码11.3  使用Boost的例子

public static void buildIndex() throws Exception {

//生成新的Document对象,下同

Document doc1 = new Document();

doc1.add(Field.Text("contents", "word1 word"));

doc1.add(Field.Keyword("path", "path//document1.txt"));

//改变文档的boost因子,下同

doc1.setBoost(1.0f);

Document doc2 = new Document();

doc2.add(Field.Text("contents", "word2 word"));

doc2.add(Field.Keyword("path", "path//document2.txt"));

doc2.setBoost(0.1f);

Document doc3 = new Document();

doc3.add(Field.Text("contents", "word3 word"));

doc3.add(Field.Keyword("path", "path//document3.txt"));

doc3.setBoost(0.5f);

Document doc4 = new Document();

doc4.add(Field.Text("contents", "word4 word"));

doc4.add(Field.Keyword("path", "path//document4.txt"));

doc4.setBoost(0.2f);

Document doc5 = new Document();

doc5.add(Field.Text("contents", "word5 word"));

doc5.add(Field.Keyword("path", "path//document5.txt"));

doc5.setBoost(0.8f);

Document doc6 = new Document();

doc6.add(Field.Text("contents", "word6 word"));

doc6.add(Field.Keyword("path", "path//document6.txt"));

doc6.setBoost(0.1f);

Document doc7 = new Document();

doc7.add(Field.Text("contents", "word7 word"));

doc7.add(Field.Keyword("path", "path//document7.txt"));

doc7.setBoost(0.5f);

Document doc8 = new Document();

doc8.add(Field.Text("contents", "word8 word"));

doc8.add(Field.Keyword("path", "path//document8.txt"));

doc8.setBoost(0.7f);

Document doc9 = new Document();

doc9.add(Field.Text("contents", "word9 word"));

doc9.add(Field.Keyword("path", "path//document9.txt"));

doc9.setBoost(0.2f);

Document doc10 = new Document();

doc10.add(Field.Text("contents", "word10 word"));

doc10.add(Field.Keyword("path", "path//document10.txt"));

doc10.setBoost(0.4f);

Document doc11 = new Document();

doc11.add(Field.Text("contents", "word11 word"));

doc11.add(Field.Keyword("path", "path//document11.txt"));

Document doc12 = new Document();

doc12.add(Field.Text("contents", "word12 word"));

doc12.add(Field.Keyword("path", "path//document12.txt"));

IndexWriter writer = new IndexWriter("c://index", new StandardAnalyzer(), true);

//添加到索引中,下同

writer.addDocument(doc1);

writer.addDocument(doc2);

writer.addDocument(doc3);

writer.addDocument(doc4);

writer.addDocument(doc5);

writer.addDocument(doc6);

writer.addDocument(doc7);

writer.addDocument(doc8);

writer.addDocument(doc9);

writer.addDocument(doc10);

writer.addDocument(doc11);

writer.addDocument(doc12);

writer.close();

}

代码11.3的运行效果,如图11-7所示。

图11-7  改变Boost后的运行效果

从图11-7可以看出,每个文档的分值已经发生了变化,其中,由于文档1、11、12的boost值和原来一样,因此分值排在最前面,显示的顺序也到了最前面。而其他的文档则已经因为boost值发生了改变,显示的顺序也发生了变化。可以看到,排在最后一个位置的文档是文档6,它的boost值为0.1,所以分值也成了原来的十分之一。

像代码11.3中这样通过Boost值来改变分值的方式相当灵活,可以很有效的达到对文档顺序进行控制的目的。不过,这仍然不是一种理想的方式,因为在建立索引时还需要人为地指定每个文档的boost值。

 

Lucene 的 Scoring 评分机制

 

Lucene 评分体系/机制(lucene scoring)是 Lucene出名的一核心部分。它对用户来说隐藏了很多复杂的细节,致使用户可以简单地使用lucene。但个人觉得:如果要根据自己的应用调节评分(或结构排序),十分有必须深入了解 lucene 的评分机制。

Lucene scoring 组合使用了??和?布尔模型?。

首先来看下 lucene 的评分公式(在 Similarity 类里的说明)

score(q,d) ? =??coord(q,d)·??queryNorm(q) ·∑(?tf(t in d)·??idf(t)2?·??t.getBoost·??norm(t,d)?)?t in q?

其中:

  • tf(t ind)?关联到项频率,项频率是指?项?t在?文档?d 中出现的次数frequency。默认的实现是:
    tf(t in d) =frequency?
  • idf(t)?关联到反转文档频率,文档频率指出现?项?t的文档数?docFreq。docFreq 越少 idf就越高(物以稀为贵),但在同一个查询下些值是相同的。默认实现:
    idf(t) =1 +log?(numDocs–––––––––docFreq+1)
  • coord(q,d)?评分因子,是基于文档中出现查询项的个数。越多的查询项在一个文档中,说明些文档的匹配程序越高。默认是出现查询项的百分比。
  • queryNorm(q)查询的标准查询,使不同查询之间可以比较。此因子不影响文档的排序,因为所有有文档都会使用此因子。默认值:
    queryNorm(q) ? =??queryNorm(sumOfSquaredWeights)=1––––––––––––––sumOfSquaredWeights?每个查询项权重的平分方和(sumOfSquaredWeights)由 Weight 类完成。例如 BooleanQuery地计算: 

    sumOfSquaredWeights =??q.getBoost?2?·∑(?idf(t)·??t.getBoost?)?2?t in q

  • t.getBoost查询时期的 项 t 加权(如:java^1.2),或者由程序使用setBoost。
  • norm(t,d)压缩几个索引期间的加权和长度因子:
    • Document boost?- 文档加权,在索引之前使用doc.setBoost
    • Field boost?- 字段加权,也在索引之前调用field.setBoost
    • lengthNorm(field)?- 由字段内的Token 的个数来计算此值,字段越短,评分越高,在做索引的时候由 Similarity.lengthNorm 计算。

    以上所有因子相乘得出 norm 值,如果文档中有相同的字段,它们的加权也会相乘:norm(t,d) ? =??doc.getBoost·??lengthNorm(field) ·∏f.getBoost?field?f?in?d?namedas?t?

    索引的时候,把 norm 值压缩(encode)成一个 byte 保存在索引中。搜索的时候再把索引中 norm值解压(decode)成一个 float 值,这个 encode/decode 由 Similarity提供。官方说:这个过程由于精度问题,以至不是可逆的,如:decode(encode(0.89)) = 0.75。

    计算这个评分涉及到几个核心的类/接口:Similarity、Query、Weight、Scorer、Searcher,由它们或其子类来完成评分的计算。先来看下它们的类图:

     

    lucene search score uml, 点击放大

    搜索中,评分的过程:

  • 创建一个查询对象 Query,传给 Searcher,具体来讲可能是 IndexSearcher。
  • Searcher 根据 Query 创建一个对应的 Weight(是 Query 的内部特征表示),接着 Weight会创建对应的 Scorer。
  • Searcher 会创建 Hitcollector 并传到 Scorer,scorer 找到匹配的文档并计算评分,最后写到Hitcollector 中。Query、Weight、Scorer 三都关系十分密切,尤其是 Query 和 Weight。Weight是计算查询权重和创建 Scorer 的。Query 为了可以重用把内部的特征抽象为Weight,由子类去完成一些相关评分的计算。任何 Searcher 依赖的状态都存储在 Weight 实现中,而不是在Query 中,这样可以重用 Query。

    Weight 的生命周期(被使用):

  • Weight 由顶层的 Query 创建。Query.createWeight(Searcher),创建的 Weight 给Searcher 去使用。
  • 当用 Similarity.queryNorm(float) 来计算查询标准化因子(querynormalization)的时候,Weight.sumOfSquaredWeights 会被调用。
  • 查询标准化因子(query normalization)会传给Weight.normalize(float)计算,这个时候权重(weighting)计算完成。
  • 创建一个 Scorer。自定义评分的计算可以实现一个 Similarity 换掉默认的。它仅限于 Scorer、Weight计算好的因子值再加工。要想对评分有更强的控制力,可以实现一套 Query、Weight、Scorer。
    • Query 是用户信息需要的抽象
    • Weight 是 Query 的内部特性表示的抽象
    • Scorer 抽象公用的计算评分功能,提供计算评分和解说(explanation)评分的能力。

    Query 子类实现的方法:

  • createWeight(Searcher searcher) -- Weight 是 Query 内部代表,所以每个Query 都必实现一个 Weight,此方法就是生成一个Query对应的Weight对象。
  • rewrite(IndexReader reader) --重写查询为原始的查询,原始的查询有:TermQuery,BooleanQuery……Weight 接口方法:
  • Weight#getQuery -- 指出代表 Weight 的 Query。
  • Weight#getValue -- Query 的权重,例如:TermQuery.TermWeight 的 value =idf^2 * boost * queryNorm
  • Weight#sumOfSquaredWeights -- 各查询项的平方和,如,TermWeight 的 = (idf *boost)^2
  • Weight#normalize(float) -- 决定查询标准化的因子,查询标准化值可以在不同 Query 比较score
  • Weight#scorer(IndexReader) -- 创建 Query 对应的评分器 Scorer,它的责任是给Query 匹配到的文档评分。
  • Weight#explain(IndexReader, int) -- 给指定的文档详细解说评分值是怎么得来了。Scorer 子类实现的方法:
  • Scorer#next -- 预取匹配到的下一文档,有才返回 true。
  • Scorer#doc -- 返回当前匹配到的文档id,它必须 next 调用后才有效。
  • Scorer#score -- 返回当前文档的评分,此值可以由应用程序以任何适当的方式给出,如 TermScorer 返回tf * Weight.getValue * fieldNorm
  • Scorer#skipTo(int) -- 跳到大于或等于 int 的匹配文档上。很多情况下,在结果集中 skipTo比较循环更加快速高效。
  • Scorer#explain(int) -- 给出评分产生的细节。要实现一套 Query、Weight、Scorer,最好还是看下TermQuery、TermWeight、TermScorer。当 Lucene 中没有想要的查询时(包括不同的评分细节),自定义Query 可能帮得上忙。
  • 发表评论