elasticsearch boosting - yaokun123/php-wiki GitHub Wiki

使用相关性进行搜索

一、Elasticsearch的打分机制

确定文档和查询有多么相关的过程被称为打分(scoring)。尽管精确地理解Elasticsearch是如何计算文档得分这一点并不是必需的,但是对于如何使用Elasticsearch而言,它仍然是非常有帮助的。

1.1 文档打分是如何运作的

Lucene(以及其扩展Elasticsearch)的打分机制是一个公式,将考量的文档作为输入,使用不同的因素来确定该文档的得分。我们先讨论每个因素,然后通过这个公式将它们综合,来更好地解释整体的得分。正如之前所提,我们希望更为相关的文档被优先返回,在Lucene和Elasticsearch中这种相关性被称为得分。

在开始计算得分之时,Elasticsearch使用了被搜索词条的频率以及它有多常见来影响得分。一个简短的解释是,一个词条出现在某个文档中的次数越多,它就越相关。但是,如果该词条出现在不同的文档的次数越多,它就越不相关。这一点被称为TF-IDF(TF是词频,即term frequency),IDF是逆文档频率(inverse document frequency),现在我们将深入讨论每种类型的频率。

1.2 词频

考虑给一篇文档打分的首要方式,是查看一个词条在文本中出现的次数。举个例子,如果在用户的区域搜索关于Elasticsearch的get-together,用户希望频繁提及Elasticsearch的分组被优先展示出来。

图6-1 词频是一个词条在文档中的出现次数 第一个句子提到Elasticsearch一次,而第二个句子提到Elasticsearch两次,所以包含第二句话的文档应该比包含第一句话的文档拥有更高的得分。如果我们要按照数量来讨论,第一句话的词频(TF)是1,而第二句话的词频将是2。

1.3 逆文档频率

比文档词频稍微复杂一点的是逆文档频率(IDF)。这个听上去很酷炫的描述意味着,如果一个分词(通常是单词,但不一定是)在索引的不同文档中出现越多的次数,那么它就越不重要。使用几个例子更容易解释这一点。

词条“Elasticsearch”的文档频率是2(因为它出现在两篇文档中)。文档频率的逆源自得分乘以1/DF,这里DF是该词条的文档频率。这就意味着,由于词条拥有更高的文档频率,它的权重就会降低。

词条“the”的文档频率是3,因为它出现在所有的3篇文档中。请注意,尽管“the”在最后一篇文档中出现了两次,它的文档频率还是3。这是因为,逆文档频率只检查一个词条是否出现在某文档中,而不检查它出现多少次。那个应该是词频所关心的事情!

逆文档频率是一个重要的因素,用于平衡词条的词频。举个例子,考虑有一个用户搜索词条“the score”,单词the几乎出现在每个普通的英语文本中,如果它不被均衡一下,单词the的频率要完全淹没单词score的频率。逆文档频率IDF均衡了the这种常见词的相关性影响,所以实际的相关性得分将会对查询的词条有一个更准确的描述。

一旦词频TF和逆文档频率IDF计算完成,就可以使用TF-IDF公式来计算文档的得分。

1.4 Lucene评分公式

二、boosting

boosting是一个可以用来修改文档的相关性的程序。boosting有两种类型。当索引或者查询文档的时候,可以提升一篇文档的得分。在索引期间修改的文档boosting是存储在索引中的,修改boosting值唯一的方法是重新索引这篇文档。鉴于此,我们当然建议用户使用查询期间的boosting,因为这样更为灵活,并允许用户改变主意,在不重新索引数据的前提下改变字段或者词条的重要性。

2.1 索引期间的boosting

当进行这种类型的boosting时,需要使用boost参数来设置字段的映射。例如,为了对group类型的name字段进行boost

localhost:9200/get-together
    "mappings":{
        "group":{
            "properties":{
                "name":{
                    "boost":2.0,//索引期间,boosting name字段的值
                    "type":"string"
                }
            }
        }
    }

在设置该索引的映射后,任何自动索引的文档就拥有一个boost值,运用于name字段的词条中(存储在Lucene索引的文章中)。再次强调一下,请记住这个boost的值是固定的(fixed),也就是说如果决定修改这个值,你必须重新索引文档。

不鼓励索引期间boosting的另一个原因是,boost的值是以低精度的数值存储在Lucene的内部索引结构中。只有一个字节用于存储浮点型数值,所以计算文档的最终得分时可能会丢失精度。

不鼓励索引期间boosting的最后一个原因是,boost是运用于一个词条的。因此,在被boost的字段中如果匹配上了多个词条,就意味着多次的boost,将会进一步增加字段的权重。

由于索引期间boosting的这些问题,最好是在进行查询的时候进行boost

2.2 查询期间的boosting

当进行搜索的时候,有几种方法进行boosting。如果使用基本的match、multi_match、simple_query_string或query_string查询,就可以基于每个词条或者每个字段来控制boost。几乎所有的Elasticsearch查询类型都支持boosting。如果这个还不够灵活,那么可以通过function_score查询,以更为精细的方式来控制boosting。

通过match查询,用户可以使用额外的boost参数来boost查询

"query":{
    "bool":{
        "should":[
            {
                "match":{
                    "description":{
                        "query":"elasticsearch big data",//查询期间,对这个match查询进行boosting
                        "boost":2.5
                    }
                }
            },
            {
                "match":{
                    "name":{
                        "query":"elasticsearch big data"//对于第二个match查询,不进行任何boosting
                    }
                }
            }
        ]
    }
}

2.3 跨越多个字段的查询

对于跨越多个字段的查询,如multi_match查询,也可以使用多个替换的语法。用户可以指定整个multi_match的boost,和刚刚看到的使用boost参数的match查询类似

 "query":{
    "multi_match":{
       "query":"elasticsearch big data",
       "fields":["name","description"],
       "boost":2.5
  }
 } 

或者可以使用特殊的语法,只为特定的字段指定一个boost。通过在字段名称后面添加一个“^”符号和boost的值,用户可以告诉Elasticsearch只对那个字段进行boost。

 "query":{
  "multi_match":{
   "query":"elasticsearch big data",
   "fields":["name^3","description"] ←——使用^3 后缀,name 字段被boost 了3 倍
  }
 }

在query_string查询中,可以使用特殊的语法来boost单个词条,在词条的后面添加^字符和boost的值。

 "query":{
  "query_string":{
   "query":"elasticsearch^3 AND \"big data\"" ←——使用^3 后缀,指定的词条被boost 了3 倍
  }
 }

三、使用“解释”来理解文档是如何被评分的

这些被称为对得分进行解释(explaining),可以通过指定explain=true来告诉Elasticsearch运行这个操作。既可以在发送请求的URL里设置,也可以在请求的主体中将explain设置为true。解释为什么一篇文档获得特定的得分是很有价值的,而且它还有另一个用处:解释为什么一篇文档无法和某个查询匹配。如果用户期望某篇文档和某个查询匹配,但是这篇文档却没有在结果集合中返回,那么这个解释就非常有帮助了。

localhost:9200/get-together/_search?pretty
{
    "query":{
        "match":{}
    },
    "explain":true//在请求主体中设置explain的旗标
}

结果中的   
"_explanation":{  ←——_explain 部分包含了对于文档得分的解释
    

释一篇文档不匹配的原因

localhost:9200/get-together/4/_explain
{
    "query":{
        "match":{
            "description":"elasticsearch"
        }
    }
}

四、使用查询再打分来减小评分操作的性能影响

我们尚未讨论打分对于系统速度的影响。在大多数正常的查询中,计算文档的得分只需要少量的开销。这是由于Lucene团队已经深度优化了TF-IDF,使其变得非常有效率。但是,在下列情况下,打分可能会变成资源密集型的操作。

使用脚本的评分,运行了一个脚本来计算索引中每篇文档的得分。

进行phrase词组查询,搜索在一定距离内出现的单词,使用很大的slop值

在这些情况下,你可能希望在成千上万的文档上运行时,减轻打分算法所产生的性能影响。

为了解决这个问题,Elasticsearch有一个特性称为再打分。再打分(rescoring)意味着在初始的查询运行后,针对返回的结果集合进行第二轮的得分计算,它也因此而得名。这意味着,对于可能非常耗费性能的脚本查询,可以先使用更为经济的match匹配查询进行搜索,然后只对前1,000项检索到的命中执行该脚本查询。

localhost:9200/get-together/_search?pretty
"query":{
    "match":{
        "title":"elasticsearch"//在所有文档执行的出事查询
    }
},
"rescore":{
    "window_size":20,//运行再评分的结果数量
    "query":{
        "match":{//将在初始查询的前20项结果上运行的新查询
            "title":{
                "type":"phrase",
                "query":"elasticsearch hadoop",
                "slop":5
            }
        }
    },
    "query_weight":0.8,//初始查询得分权重
    "rescore_query_weight":1.3//再评分查询得分的权重
}

这个例子搜索了所有标题中含有“Elasticsearch”关键词的文档,然后获取前20项结果,然后对它们重新计算得分,它使用了高slop值的phrase查询。尽管高slop值的phrase查询是很耗费性能的,你也没有必要担心,因为这个查询只会在前20篇文档上执行,而不是成千上万可能相关的文档。用户可以使用query_weight和rescore_query_weight参数来权衡不同查询的重要性,这取决于你希望最终的得分多少是由初始查询决定,多少是由再评分查询决定。用户可以按序使用多个rescore再评分查询,每个查询使用前面的结果作为输入。

五、使用function_score来定制得分

function_score查询允许用户指定任何数量的任意函数,让它们作用于匹配了初始查询的文档,修改其得分,从而达到精细化控制结果相关性的目的。

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[]//空白的函数列表
    }
}

足够简单,看上去就像一个普通的match匹配查询,被封装在了一个function_score查询当中。它有了一个新的键:functions。它目前是空的,不过不用担心,很快就要向这个数组添加内容了。这个代码清单是为了展示这个查询的结果,就是function_score函数所要操作的文档。举个例子,如果索引中总共有30篇文档,而某个match查询,它要在description字段上搜索关键词“elasticsearch”,结果匹配上其中的25篇,那么数组中的函数将会应用在这25篇文档上。

5.1 weight函数

weight函数是最简单的分支,它将得分乘以一个常数。请注意,普通的boost字段按照标准化来会增加分数,而weight函数却真真切切地将得分乘以确定的数值。

前面的例子已经找到了所有在description字段中包含“elasticsearch”的文档,而如下代码将boost在description字段中包含“hadoop”的文档。

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[
            {
                "weight":1.5,
                "filter":{"term":{"description":"hadoop"}}//权重函数将description中含有hadoop关键词的文档boost了1.5
            }
        ]
    }
}

5.2 合并得分

现在讨论一下,这些得分是如何合并的。当谈及这些分数时,有两种不同的因素需要探讨:

从每个单独的函数而来的得分是如何合并的,这被称为score_mode。

从函数而来的得分是如何同原始查询(这个例子中,是在description字段搜索“elasticsearch”)得分相合并的,这被称为boost_mode。

第一个因素被称为score_mode参数,它处理不同函数得分是如何合并的。在之前cURL的请求中有两个函数:一个权重是2,另一个权重是3。用户可以设置score_mode参数为multiply、sum、avg、first、max和min。如果没有特别指明,每个函数的得分是相乘的。

如果指定了first,只会考虑第一个拥有匹配过滤器的函数的分数。举例来说,如果将score_mode设置为first,并且有一篇文档的描述中有“hadoop”和“logstash”关键词,那么只会实施值为2的boost因子,因为这是第一个匹配文档的函数。

第二种得分合并的设置,被称为boost_mode,它控制了原始查询的得分和函数得分是如何合并的。如果没有指定,新的得分是初始查询得分和函数得分相乘。用户可以将其设置为sum、avg、max、min或者replace。设置为replace,意味着原有的查询得分将会被函数得分所替换。

有了这些设置,你就可以学习下一个function_score函数,它会根据字段取值来修改得分。我们将涵盖的函数将包括field_value_factor、script_score和random_score,还有3种衰减功能:linear、gauss和exp。这里将从field_value_factor函数开始。

5.3 field_value_factor函数

根据其他的查询来修改得分是非常有用的,但是许多用户希望使用文档中的数据来影响文档的得分。在这个例子中,你可能想使用事件所获得的评论数量来增加事件的得分。在function_score查询中,使用field_value_factor函数将使得这一点成为可能。

field_value_factor函数将包含数值的字段的名称作为输入,选择性地将其值乘以常数,然后最终对其运用数学函数,如取数值的对数。、

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[
            {
                "field_value_factor":{
                    "field":"reviews",//用作数值的数值型字段
                    "factor":2.5,//评论字段将要乘以的因子
                    "modifier":"In"//可选的修饰符,用于计算得分
                }
            }
        ]
    }
}

除了ln之外,还有其他的修改函数:none(默认的)、log、log1p、log2p、ln1p、ln2p、square、sqrt和reciprocal。当使用field_value_factor的时候,有一件事情需要记住:它将所有用户指定的字段值加载到内存中,因此可以很快地计算出得分。

5.4 脚本

脚本评分可以让用户完全地控制如何修改得分。用户可以在脚本中进行任何的排序。

简短地回顾一下,脚本是以Groovy语言书写的,而且可以在脚本中使用_score来访问文档初始的得分。用户可以使用doc['fieldname']来访问文档某个字段的值。

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[
            {
                "script_score":{
                    //将在每篇文档上运行的脚本,它会决定得分的数值
                    "script":"Math.log(doc['attendees'].values.size() * myweight)",
                    "params":{"myweight":3}//变量myweight 将会被请求中的参数所替代
                }
            }
        ],
        "boost_mode":"replace"//初始的文档得分将会被脚本产生的得分所替代
    }
}

这个例子将使用参与者(attendees)列表的人数来影响得分,将其和权重相乘,并取对数。

脚本是非常强大的,因为在其中可以做任何喜欢的事情。但是请记住脚本比普通的评分操作要慢得多,原因是对于每篇匹配查询的文档而言,它们必须是动态执行的。当使用代码清单中的参数化脚本时,对脚本进行缓存将有助于性能的提升。

5.5 随机

Random_score函数给予用户为文档指定随机分数的能力。能够随机排列文档的优势在于,为结果的首页引入了一定的变化。当搜索get-together的时候,偶尔看到不尽相同的前列结果有时是件好事。

用户也可以选择性地指定种子(seed),这是一个传送给查询的数值,它将用于产生随机数。这一点可以让用户以随机的方式来排列文档,但是使用同样的随机种子,再次执行同样的请求时,结果排序将总是一样的。

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[
            {
                "random_score":{
                   "seed":1234//Random_score函数的可选种子
                }
            }
        ]
    }
}

5.6 衰减函数

function_score中最后一组函数是衰减函数。它们允许用户根据某个字段,应用一个逐步衰减的文档得分。在某些情况下这一点是非常有用的。例如,用户希望让最近举办的get-together聚会有更高的得分,随着get-together举办的时间越来越久远,分数将会逐渐地减少。另一个例子是地理位置的数据,使用衰减函数可以对靠近某个地理点(如一位用户的位置)的结果增加得分,并对逐步远离该点的结果逐渐减少得分。

一共有3种类型的衰减函数,即linear、gauss和exp。每种衰减函数遵循同种语法。

{
"TYPE":{ 
  "origin":"……",
  "offset":"……",
  "scale":"……",
  "decay":"……"
}
}

其中,TYPE可以是3种衰减函数的任意一种类型。每种类型对应于一个不同形状的曲线,如图所示。

5.7配置选项

配置选项定义了曲线看上去是什么样子的。对于每种衰减曲线,有以下4种配置选项。

origin是曲线的中心点,在这里用户希望分数是最高的。在地理距离的例子中,origin很可能是一位用户现在的位置。在其他的情况下,原点可以是日期或者是数值型字段。

offset是分数开始衰减的位置,和原点之间的距离。在我们的例子中,如果offset设置为1km,这就意味着距离原点1公里内的点,分数不会被减少。位移默认的值是0,意味着数值一旦离开原点的值,分数将立即开始衰减。

scale和decay这两个选项是密切合作的。通过设置它们,可以让字段值为指定的scale值时,其得分减少到指定的decay。听上去有些困惑?将其想象为设定实际的数值,就更容易理解了。如果将scale设置为5km,将decay设置为0.25,那么这就是说“在距离原点5公里的地方,分数应该是原点分数的0.25倍。”

"query":{
    "function_score":{
        "query":{
            "match":{
                "description":"elasticsearch"
            }
        },
        "functions":[
            {
                "gauss":{
                   "geolocation":{
                  "origin":"40.018528,-105.275806", ←——衰减开始的原点位置
                  "offset":"100m", ←——在距离原点100 米之内的位置,分数保持不变
                  "scale":"2km", ←——距离原点2 公里的地方,分数将被降低一半
                  "decay":0.5
               }
                }
            }
        ]
    }
}