受欢迎的博客标签

Elasticsearch的评分机制原理

Published
Elasticsearch是基于Lucene的,所以他的默认评分机制也是基于Lucene的。如果没有干预评分算法的时候,每次查询,Lucene会基于一个评分算法来计算所有文档和搜索语句的相关评分。 Lucene的评分公式 Lucene的评分使用了TF/IDF算法,这是一种基于词频的算法。 TF (Term Frequency)代表分词项在被匹配的文档中出现的次数。 IDF (Inverse Document Frequency)代表分词项在所有文档中出现的频率。 简单来说就是将搜索的短语进行分词得出分词项,每个分词项和每个索引中的文档根据TF/IDF进行词频出现的评分计算。然后将每个分词项的得分相加,就得到了这个搜索对应的文档得分。 具体的评分公式如下: 这个评分公式有6个部分组成 tf(t in d) 指项t在文档d中出现的次数frequency。具体值为次数的开根号。 idf(t) 反转文档频率, 出现项t的文档数docFreq t.getBoost 查询时候查询项加权 norm(t,d) 长度相关的加权因子 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中出现的次数(frequency)。具体计算时,取值为次数的平方根。 public float tf(float freq) {    return (float)Math.sqrt(freq); } 比如有个文档叫做"this is book about chinese book", 我的搜索项为"book",那么这个搜索项对应文档的freq就为2,那么tf值就为根号2,即1.4142135。 idf 反转文档频率, 指包含项t的文档在所有文档中出现的频率。 public float idf(long docFreq, long numDocs) {    return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0); } 其中, docFreq 指的是项出现的文档数,就是有多少个文档符合这个搜索。 numDocs 指的是索引中总共有多少个文档。 在使用Elasticsearch时,会发现numDocs数和实际的文档数不一致。这是因为Elasticsearch是由多个shard组成,每个shard都是一个独立的Lucene索引,这里的numDocs指的是文档所在shard的文档数,而不是所有shard的文档数。所以使用Elasticsearch分析这个公式的时候,最好将分片数设置为1。 假设现在有三个文档,分别为: 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) 这个项是长度的加权因子,目的是为了将同样匹配的文档,比较短的放比较前面。比如两个文档: chinese chinese book 我搜索chinese的时候,第一个文档会放比较前面。因为它更符合"完全匹配"。 norm(t,d) = doc.getBoost()· lengthNorm· ∏ f.getBoost() 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,那么nor(t,d)就和lengthNorm一样的值。比如现在有一个文档: chinese book 搜索的词语为chinese, 那么numTerms为2,lengthNorm的值为 1/sqrt(2) = 0.71428571428571。 但是非常遗憾,如果你使用explain去查看Elasticsearch的时候,会发现lengthNorm显示的只有0.625。这个官方给出的原因是精度问题,norm在存储的时候会进行压缩,查询的时候进行解压,而这个解压是不可逆的,即decode(encode(0.714)) = 0.625。 示例 Elasticsearch中可以使用_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    .