lucene的相关度打分

term:不是单纯的key。是field-key,指定域下面的key
 
影响打分的因素
coord:document命中query中term的个数(不是算数量,是算不同term的个数)
 
term.tf:term在对应域里出现的频率
term.idf:包含这个term的document个数
 
query.boost:query里权重(search的时候设定)
term.boost:query里term的权重(search的时候设定)
doc.boost:doc的权重(写入doc的时候已经确定)
field.boost:term对应域的权重(写入doc的时候已经确定)
 
field.norm:term对应域的长度(和长度成反比)
========================================================================================

 lucene 的评分公式

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

 

这个公式是由基本的空间向量公式经过化简后再加入各种boost变形而来

 

首先来解释文章相关度和空间向量之间的概念转换

 

1 每篇文章都由多种不同的term组成,如果说每种term都表示一个独立的内容含义,那么以多维空间表示的话,每种term都是该空间的一个方向向量。

2 文章内容要表达的总的含义也是一个方向向量,这个方向向量的方向和模受这篇文章内所有term向量的影响(也可以说文章总的意思的方向向量是由所有term向量共同组合而成)。

3 如果某个term向量的模要明显长于其他term向量,就更能引导总向量的方向偏向于它(比如V1+V2=V3,那么如果V1的模比V2要大很多很多,其实V3无限逼近V1的方向)。所以说每个term向量的模都在影响着总向量的方向(其实就是文章表达的总含义)。那么如果term向量的模大小是由对应term的tf和idf来表示,那么就可以建立tf,idf和文章总向量方向之间的数学关系——即tf和idf能左右文章的总含义,这样就把各term的tf,idf和文章总含义用数学关系串起来了。

4 从上面得到的数学模型就是文章中各种term的tf和idf共同影响着文章总的意思,

doc的向量可表示为

Vd=(d_t1_tf * d_t1_idf, d_t2_tf * d_t2_idf, d_t3_tf * d_t3_idf, ...d_tN_tf * d_tN_idf)

d_tN_tf表示doc里第N个term的tf

d_tN_idf表示doc里第N个term的idf

 

而查询语句也可以看成是一篇简短的doc 所以query的向量可以表示为

Vq=(q_t1_tf * q_t1_idf, q_t2_tf * q_t2_idf, q_t3_tf * q_t3_idf, ...q_tN_tf * q_tN_idf)

q_tN_tf表示query里第N个term的tf

q_tN_idf表示query里第N个term的idf

 
从上面123点的表诉可以知道V_doc和V_query分别表示的doc和query的总的含义,而这2个向量的夹角越小则方向越靠近也就说明两者表达的意思越相近——相关度越高
 
所以我们计算夹角的余弦值作为相关性的打分,余弦值越大->夹角越小->相关性越大
 
 
下一步就是把公式转化成lucene的标准形式
 
分子
Vq*Vd=(d_t1_tf * d_t1_idf, ...d_tN_tf * d_tN_idf) * (q_t1_tf * q_t1_idf,  ...q_tN_tf * q_tN_idf) 
          =d_t1_tf * d_t1_idf * q_t1_tf * q_t1_idf + ... + d_tN_tf * d_tN_idf * q_tN_tf * q_tN_idf
 
1.因为query是查询语句,所以可认为大多数情况下每个查询的term在query仅仅会一次,所以q_tN_tf=1
2 因为d_t1_idf 和 q_t1_idf都代表term在全文档中的重要程度,所以2个值没啥区别 可看成tN_idf
 
所以分子简化成
Vq*Vd=d_t1_tf * t1_idf * t1_idf + ... + d_tN_tf * tN_idf * tN_idf

即:

( d_tN_tf ·  tN_idf2  )

                                                               1->N

然后是分母

|V(q)| =((q_t1_tf * q_t1_idf)2 + ...+(q_tN_tf * q_tN_idf)2

因为q_tN_tf上面说过可看成1,所以

|V(q)| =(q_t1_idf2 + ...+ q_tN_idf2

 

(∑ ( q_tN_idf2  ))½

                                                                    1->N

 

|V(d)|是doc的总长度

由于Vq*Vd=d_t1_tf * t1_idf * t1_idf + ... + d_tN_tf * tN_idf * tN_idf,

所以如果先不看|V(q)|

V(q) · V(d)                                                                                                             1
––––––––– = (d_t1_tf * t1_idf2 + ... + d_tN_tf * tN_idf2) * –––– 
     |V(d)|                                                                                                              |V(d)| 

                        d_t1_tf * t1_idf2                   d_tN_tf * t1_idf2

                    =  ––––––––––––  +...+    –––––––––––– 

                                  |V(d)|                             |V(d)| 

在这个算式中每个分项式分子的逻辑含义是表示每个term在对应field内的相关度分值(结合它在其对应field里的频率(tf)和该term本身在全文档中的重要程度(idf)算得),而相除的分母一律是|V(d)| (doc的总长度),这样相除会导致相关度值不够准确

因为在索引中,不同的文档长度不一样,很显然,对于任意一个term,在长的文档中的tf要大的多,因而分数也越高,这样对小的文档不公平,举一个极端的例子,在一篇1000万个词的鸿篇巨著中,"lucene"这个词出现了11次,而在一篇12个词的短小文档中,"lucene"这个词出现了10次,如果不考虑长度在内,当然鸿篇巨著应该分数更高,然而显然这篇小文档才是真正关注"lucene"的。

所以分母不应该是|V(d)| (doc的总长度),而应该分别是分子上每个term对应的域上的term总个数,即“num of term in field f”

就是lucene里面的lengthNorm 这个值等于 (1.0 / Math.sqrt(“num of term in field f”)

所以上面那个连加算式可转化为

 即

( (d_tN_tf ·  tN_idf2 )/(“num of term in field f”)½)

                                                1->N

再加上分母的|V(q)|:            

 

 

这个就是通过纯粹向量公式再加上lucene对里面一些指标的修改转化而成的原型公式,如果在乘上各种boost就和文档上写的总公式一样了。

 

上面说了lucene采用向量公式时对公式里的几个因子进行了简化和修改,总结一下

1 q_tN_tf(query中term的频率)简化成了1

2 d_t1_idf 和 q_t1_idf,都代表term在全文档中的重要程度,所以2个值没啥区别 可看成一样

3  |V(d)|(doc长度),按照向量公式转化后每个term在对应field里相关度分数要和 |V(d)|做除,这样不符合实际相关度的情况(理由上面说明),所以去除了|V(d)|,用lengthNorm(代表分子上每个term所在域的term总个数)代替。

 

公式中关于各类因素的具体实现

public float coord(int overlap, int maxOverlap) {
return overlap / (float)maxOverlap;
}

 

/** Implemented as <code>1/sqrt(sumOfSquaredWeights)</code>. */
@Override
public float queryNorm(float sumOfSquaredWeights) {
return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
}

 

public float lengthNorm(FieldInvertState state) {
final int numTerms;
if (discountOverlaps)
numTerms = state.getLength() - state.getNumOverlap();
else
numTerms = state.getLength();
return state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms)));
}

 

public float tf(float freq) {
return (float)Math.sqrt(freq);
}

/** The default implementation returns <code>1</code> */
@Override
public float scorePayload(int doc, int start, int end, BytesRef payload) {
return 1;
}

 

/** Implemented as <code>log(numDocs/(docFreq+1)) + 1</code>. */
@Override
public float idf(long docFreq, long numDocs) {
return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
}

 

 

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。