1. Elasticsearch中的相关性计算
在正式进入算法解析阶段之前,先一步一步的补足相关的概念知识,这会帮助我们更好的学习和理解。
1.1 什么是相关性评分(relevance score)?
相关性评分(relevance score)是衡量每个文档与输入查询匹配的程度。默认情况下,Elasticsearch根据相关性评分对匹配的搜索结果进行排序。
相关性评分是一个正浮点数,在Search API的score元数据字段中返回。score越高,说明文档越相关。。。
一个简单的示例,首先通过bulk API 或者你熟悉的方式向索引里写入一些数据,这里以书名为例。
POST _bulk
{ "index" : { "_index" : "book_info", "_id" : "1" } }
{ "book_name" : "《大学》" }
{ "index" : { "_index" : "book_info", "_id" : "2" } }
{ "book_name" : "《中庸》" }
{ "index" : { "_index" : "book_info", "_id" : "3" } }
{ "book_name" : "《论语》" }
{ "index" : { "_index" : "book_info", "_id" : "4" } }
{ "book_name" : "《孟子》" }
{ "index" : { "_index" : "book_info", "_id" : "5" } }
{ "book_name" : "《道德经》" }
{ "index" : { "_index" : "book_info", "_id" : "6" } }
{ "book_name" : "《诗经》" }
{ "index" : { "_index" : "book_info", "_id" : "7" } }
{ "book_name" : "《春秋》" }
然后执行一个最简单的查询,搜索索引内书名匹配“诗经”这两个字的书籍信息
GET /book_info/_search
{
"query": {
"match": { "book_name": "诗经" }
}
}
//得到以下响应
{
"took" : 2,
........
"max_score" : 2.916673,
"hits" : [
{
"_index" : "book_info",
"_type" : "_doc",
"_id" : "6",
"_score" : 2.916673,
"_source" : {
"book_name" : "《诗经》"
}
},
{
"_index" : "book_info",
"_type" : "_doc",
"_id" : "5",
"_score" : 0.99958265,
"_source" : {
"book_name" : "《道德经》"
}
}
]
}
}
可以看到结果如同预期的,《诗经》这本书的相关性评分为 2.916673 ,而第二本书因为只匹配了一个“经”字,所以得分较低。现在我们的目的就是彻底吃透这两个分数是如何计算的,以应对实际使用时的各种问题。
1.2 相关度评分是如何计算的?
Elasticsearch是基于 Lucene 之上的搜索引擎,这部分深入的内容以及概念留到后面补充,否则只会出现片面的描述或者陷入递归式的探究中。现在,先让我们囫囵吞枣的理解眼前的事物。
Elasticsearch中的相关性评分计算可以参考Elasticsearch文档相似模块的描述,传送门:Elasticsearch | Index Modules Similarity
在不做任何配置,默认的情况下我们可以使用以下三种相似度评分算法:
- BM25:Okapi BM 25算法。在Elasticearch和Lucene中默认使用的算法。
- classic: 在7.0.0中标记为过时。基于TF/IDF 算法,以前在Elasticearch和Lucene中的默认值。
- boolean:一个简单的布尔相似度算法,当不需要全文排序时可以使用,并且分数应该只基于查询项是否匹配。布尔相似度给查询一个简单的分数,等价于设置的Query Boost。
通过以上描述我们可以了解到,Elasticsearch中默认的评分算法是BM25算法,且其他两个选项一个被标记过时,一个不适用于全文检索排序。现在实际尝试一下上面提到的三种算法,由于classic算法已经被标记过时,这里直接在Mapping中使用classic会直接抛出异常并提示我们可以使用脚本自定义实现原本的classic算法
{
"type" : "illegal_argument_exception",
"reason" : "The [classic] similarity may not be used anymore. Please use the [BM25] similarity or build a custom [scripted] similarity instead."
}
按照文档中给出的示例编写索引Mapping:
//删除之前创建的索引
DELETE book_info
//创建自定义的索引并制定字段类型、相关度评分算法
PUT book_info
{
"mappings": {
"properties": {
//默认字段依旧采用BM25,并且对该字段赋值时自动复制到下面两个字段
"book_name": {
"type": "text",
"similarity": "BM25",
"copy_to": [
"book_name_classic",
"book_name_boolean"
]
},
//这个字段使用classic相关度算法
"book_name_classic": {
"type": "text",
"similarity": "my_classic"
},
//这个字段使用boolean相关度算法
"book_name_boolean": {
"type": "text",
"similarity": "boolean"
}
}
},
"settings": {
"number_of_shards": 1,
"similarity": {
"my_classic": {
"type": "scripted",
"script": {
"source": "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;"
}
}
}
}
}
之后用与上面相同的Bulk请求填充一下数据,即可观察相关性算法配置的结果,在这里不对结果进行解析。
GET /book_info/_search
{
"query": {
"match": {
"book_name": {
"query": "诗经"
}
}
}
}
GET /book_info/_search
{
"query": {
"match": {
"book_name_classic": {
"query": "诗经"
}
}
}
}
GET /book_info/_search
{
"query": {
"match": {
"book_name_boolean": {
"query": "诗经"
}
}
}
}
2. BM 25 算法
通过第一章的描述,我们知道了现在在Elasticsearch中的相关性评分默认采用BM25相似度算法,下面正式进入算法的学习阶段。
BM25全称Okapi BM25。Okapi 是使用它的第一个系统的名称,即Okapi信息检索系统,BM则是best matching的缩写。
BM25是基于TF-IDF算法并做了改进,基于概率模型的文档检索算法,目前BM25及其较新的变体(例如BM25F)代表了文档检索中使用的最先进的TF/IDF类检索功能。
现在,抛开中文分词器、同义词、停词等一切可能的干扰项,我们就使用最基本的Standard分词器,准备一点英文文档数据:
PUT _bulk
{ "index" : { "_index" : "people", "_id" : "1" } }
{ "title": "Shane" }
{ "index" : { "_index" : "people", "_id" : "2" } }
{ "title": "Shane C" }
{ "index" : { "_index" : "people", "_id" : "3" } }
{ "title": "Shane Connelly" }
{ "index" : { "_index" : "people", "_id" : "4" } }
{ "title": "Shane P Connelly" }
下面是BM25算法的标准公式,表示给定一个查询Q,包含关键字 q{1},...,q{n},文档D的BM 25分数计算公式为
现在详细解释等式每个部分的含义
2.1 搜索项 qi及词频TF
\(q_i\) : 查询项,例如我搜索“Shane”,只有一个查询项,所以 \(q_0\) 是“Shane”。
如果我用英语搜索“Shane Connelly”,Elasticsearch将看到空格并将其标记为两个Term:
- \(q_0\) :Shane
- \(q_1\): Connelly
\(f(q_i,D)\) : D 是文档, Term Frequency (TF)是指Term在文档中出现的频率,即词频。Term在文档中出现的权重与频率成正比。用最通俗易懂的话来说就是查询的词语在文档中出现了多少次。
这很有直觉意义,例如我正在搜索Elasticsearch,一篇文章内提到了一次可能只是简单的引用,如果文章中出现了很多次Elasticsearch那就更有可能与我们的搜索内容相关。
2.2 逆文档频率 IDF
\(IDF(q_i)\): 查询项的逆文档频率,衡量这个词提供了多少信息,也就是说它在所有文档中是常见的还是罕见的。这
其中的含义是如果一个搜索词是非常罕见的词语(例如专业术语)且在某个文档中匹配了,则分数会提高;反之对于非常常见的匹配词降低分数。
TF-IDF中原本的逆文档频率公式是这样的: