得分机制和思想
对于搜索,一般包括从库里通过query搜索出docs并排序。
本质上是一个排名问题,检索的话比较简单,可以通过倒排文档的思路,直接通过词找到包括该词的文档(最原始的思路)。
lucene也是利用了两类模型:布尔模型和向量空间模型; [布尔模型]{http://blog.youkuaiyun.com/iterate7/article/details/77206613}负责检索到数据; 向量空间模型负责得分排序。
所谓的向量空间模型,可以理解为:query和doc都会映射为一个vector,通常情况下是term vectors;而权重则选择tf-idf,在同一个特征空间进行计算排序。
Lucene combines Boolean model (BM) of Information Retrieval with Vector Space Model (VSM) of Information Retrieval - documents “approved” by BM are scored by VSM.
评分公式
VSM评分公式
lucene概念评分公式
lucene实际评分公式
coord(q,d)
协调因子,文档中出现查询项的个数越多,匹配度越高。
public float coord(int overlap, int maxOverlap) {
return overlap / (float)maxOverlap;
}
overlap: 当前文档中满足检索条件的满足个数
maxOverlap: 检索条件的总个数
比如检索”english book”, 现在有一个文档是”this is an chinese book”。
那么,这个搜索对应这个文档的overlap为1(因为匹配了book,满足一个条件),而maxOverlap为2(因为检索条件有两个book和english)。
最后得到的这个搜索对应这个文档的coord值为0.5。
queryNorm(q)
查询的标准化;只是对词的标准化,不影响文档排序。只是用于不同词之间得分比较的时候用的。
公式是:
代码:
public float queryNorm(float sumOfSquaredWeights) {
return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
}
tf(t in d)
t在d中出现的次数;取平方根,为了scale吧。
比如有个文档叫做”this is book about chinese book”, 我的搜索项为”book”,那么这个搜索项对应文档的freq就为2,那么tf值就为根号2,即1.4142135
代码:
public float tf(float freq) {
return (float)Math.sqrt(freq);
}
idf(t)
逆文档频度,主要是判定该词对文档的区分度。如果很大,说明该词可以区分文档;如果是0,基本上可以认为,每个文档都有这个词,无任何区分意义。
public float idf(long docFreq, long numDocs) {
return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
}
这里的两个值解释下
docFreq 指的是项出现的文档数,就是有多少个文档符合这个搜索
numDocs 指的是索引中有多少个文档。
为了平滑(smooth)计算公式:
比如我现在有三个文档,分别为:
this book is about english
this book is about chinese
this book is about japan
我要搜索的词语是”chinese”,那么对第二篇文档来说,docFreq值就是1,因为只有一个文档符合这个搜索,而numDocs就是3。最后算出idf的值是:
(float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0) = ln(3/(1+1)) + 1 = ln(1.5) + 1 = 0.40546510810816 + 1 = 1.40546510810816
t.getBoost
查询时期项t的加权,这个就是一个影响值,比如我希望匹配chinese的权重更高,就可以把它的boost设置为2
norm(t,d)
这个是term的加权因子,目的是将同样匹配的文档,比较短的放前面。
norm(t,d) = doc.getBoost()· lengthNorm· ∏ f.getBoost()
lengthNorm代码:
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)));
}
doc.getBoost代表文档权重;
f.getBoost代表字段权重,越高代表越重要,一般默认是1.0;
lengthNorm:一个域中包含的Term总数越多,也即文档越长,此值越小,文档越短,此值越大。
所以基本上由lengthNorm来决定;
比如我现在有一个文档:chinese book
搜索的词语为chinese, 那么numTerms为2,lengthNorm的值为 1/sqrt(2) = 0.71428571428571。
但是非常遗憾,如果你使用explain去查看es的时候,发现lengthNorm显示的只有0.625。
这个官方给出的原因是精度问题,norm在存储的时候会进行压缩,查询的时候进行解压,而这个解压是不可逆的,即decode(encode(0.714)) = 0.625。
注释:
索引的时候,把 norm 值压缩(encode)成一个 byte 保存在索引中。搜索的时候再把索引中 norm 值解压(decode)成一个 float 值,这个 encode/decode 由 Similarity 提供。官方说:这个过程由于精度问题,以至不是可逆的,如:decode(encode(0.89)) = 0.75。
接下来,查看Lucene的DefaultSimilarity类源码,看下核心的几个方法代码
Java代码 收藏代码
/* Cache of decoded bytes. /
private static final float[] NORM_TABLE = new float[256];
static {
for (int i = 0; i < 256; i++) {
NORM_TABLE[i] = SmallFloat.byte315ToFloat((byte)i);
}
}
//索引期间执行,将norm编码成一个8位字节
public final long encodeNormValue(float f) {
return SmallFloat.floatToByte315(f);
}
//搜索期间执行,将norm,还原成具体的分数,参与评分
public final float decodeNormValue(long norm) {
return NORM_TABLE[(int) (norm & 0xFF)]; // & 0xFF maps negative bytes to positive above 127
}
仔细看decodeNormValue方法,这个代码,发现里面竟然有将float强制转换为int一个强转,这意味着,精度损失。 至于为什么这样?为了快速?!留待讨论。
由于是直接存储了term在doc中的norm值,检索的时候只要解码即可,这样使得速度极快!
解释和例子
es中可以使用_explain接口进行评分解释查看。
比如现在我的文档为:
chinese book
搜索词为:
{
"query": {
"match": {
"content": "chinese"
}
}
}
explain得到的结果为:
{
"_index": "scoretest",
"_type": "test",
"_id": "2",
"matched": true,
"explanation": {
"value": 0.8784157,
"description": "weight(content:chinese in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.8784157,
"description": "fieldWeight in 1, product of:",
"details": [
{
"value": 1,
"description": "tf(freq=1.0), with freq of:",
"details": [
{
"value": 1,
"description": "termFreq=1.0"
}
]
},
{
"value": 1.4054651,
"description": "idf(docFreq=1, maxDocs=3)"
},
{
"value": 0.625,
"description": "fieldNorm(doc=1)"
}
]
}
]
}
}
看到这篇文档的总得分为 0.8784157
tf(t in d): 1
idf: ln(3/(1+1)) + 1 = 1.4054651
norm(t,d): decode(encode(1/sqrt(2))) = 0.625 #
总分: 1.4054651 * 0.625 = 0.8784157
总结
- 计算的实际公式,是VSM的不断细化。是乘积形式,而且是每个特征作为一个因子;
- 协调因子负责匹配度衡量
- queryNorm负责计算query的得分(tfidf*boost)
- 然后计算文档的tfidf,t.boost
- norm(t,d); 把doc得分,域得分和长度lengthnorm-field融合在一起;而lengthNorm主要是该域包含的词多不多;多则得分低。
参考文献
https://nlp.stanford.edu/IR-book/html/htmledition/queries-as-vectors-1.html
https://en.wikipedia.org/wiki/Standard_Boolean_model
https://lucene.apache.org/core/4_9_0/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html#formula_termBoost