数据库系统主要处理结构化数据,他们只是去查询一条记录在不在db中,是否match。在全文搜索中,这种是非关系是不够的,我们需要知道match的记录到底有多match,要根据这种相关性排序。这种相关性的计算是有很多因素影响的。这些都是es强大的scoring体系架构所体现的。
1:theory behind relevance scoring
Lucene用布尔模型来找到匹配的doc,用Practial Score Function来计算相关性。Practial Score Function来源自TF/IDF和空间向量模型理论,同时添加了一些其他因子来协助计算相关性。
1.1 boolean model:
简单的与或非关系,支持AND OR NOT。
tf/idf:
一旦找到了匹配的docs,就需要计算这些doc的相关性,然后排序呈现给用户。并不是所有的doc都含有查询的term,也不是所有term的重要性都相同。score值决定于term的weight。term的weight取决于三个因素:term-frequency,inverse document frequency,field length norm。
tf----一个term在doc中出现越是频繁,其权重越高。
tf(t in d) = √ƒ requency
如果你并不关心tf,只是关注term是否出现,只需要mappin各种对指定的properties设置index_options值为“docs”即可。如果field是not_analyzed的,则docs是默认取值。注意,在phrase or proximity query是不可用的。
idf----一个term在所有doc中出现的次数越多,这个term就越不重要,说明这个term太普遍了。idf(t) = 1+ log(numDocs / (docFreq + 1))
field length norm----field的length越长,权重越小。norm(d) = 1 / √numTerms
在全文搜索中field length norm是重要的,但是某一些field并不需要这个。norm消耗内存,每一个doc的每一个string field,都会占用一个字节,无论这个doc是否含有这个field。not_analyzed field默认disable了norms选项。我们也可以设置mapping中对用property的norms属性为"enabled" : false来禁用。这样该field就不会关注field length信息。一个long field 和一个 short field在评分过程中将会认为length是相同的。
有一些应用场景下,比如logging,我们所关注的往往是一个field是否含有某一个error code,并不关注field length,这种情形下尽量禁用norms,会节省内存空间。
以上三个因素,计算和存储都是在index过程中实现的。他们共同决定最后的score。我们可以用explain方法来查看具体的信息。
1.2 空间向量模型
空间向量模型提供了一种方式来比较multi-term query,用向量来表示query和doc。输出score代表了这个doc跟query的匹配程度。
query用一个向量表示,匹配到的doc分别具体的向量表示,利用向量的相似性(倾角大小)来表示匹配程度,从而完成排序。
2:lucene's pratial scoring function
对于multi-term query,lucene采用了布尔模型,tf/idf,空间向量模型,综合使用以上模型来对doc进行scoring。
一个简单的query : {"match" : {"text" : "quick fox"}},讲会翻译成一个bool query
"bool" : { "should" : [ {"term" : { "text" : "quick"}}, {"term" : {"text" : "fox"}}]}
其中bool query实现了布尔模型,找到匹配的docs。一旦符合了布尔模型,接下来lucene就会用parctical scoring function来计算score值。
score(q,d) = queryNorm(q) · coord(q,d) · ∑ ( tf(t in d) · idf(t)² · t.getBoost() · norm(t,d) ) (t in q)从上边的公式可以看出最终score的影响因子有哪些,其中一些我们前边已经说过。
query normalization factor:
这个因子主要是用来对一个query进行normalize,以至于各个query的result 可以相互比较。其实作用不大,query的目的是对结果进行排序,对于不同query的结果进行比较意义不大。
queryNorm = 1 / √sumOƒ SquaredWeights 就是把query中所有term的idf相加
query coordinator
coord用来对含有较多query term的doc进行奖励提权。doc中出现的query term越多,说明匹配度越好,给的奖励权重就越高。
假设有也个query:quick brown fox。每一个term的weight都是1.5.如果没有coord,则score仅仅是下边这样:
doc with fox : 1.5
doc with quick fox : 3.0
doc with quick brown fox : 4.5.
加入coord,会是这样:
doc with fox : 1.5 * 1/3 = 0.5
doc with quick fox : 3.0 * 2/3 = 2.0
doc with quick brown fox : 4.5 * 3/3 = 4.5
显然coord的存在提升了匹配到更多term的doc对应的相关性。
bool query 默认启用了coord,很多情况下这是好事。但是在一些高级场景下,我们向disable这个特性。假设我们是在查找同义词:jump leap hop。我们并不关心有多少个同义词在同一个doc中出现,我们之关心是否出现。因此我们不需要coord属性,设置为"disable_coord" : true。当然我们在应用同义词的场景下,这个选项是默认关闭的,从应用的角度看,不需要担心。
index time field-level boosting:
我们可以提升某个field的权重,使得它比其他field更重要些。我们可以在query time进行设置。也支持在index time进行设置。事实上,boost是应用到field的所有term中。为了存储这个boost value在index中,又不想占用过多内存,field-level index-time boost 和 field-length-norm绑定在一起,占用一个字节。对应到上边公式中的norm(t,d)。
es不推荐使用field-level index-time boost,原因有:
将field-level index-time boost和norm绑定存储,使得norm丧失了精准度。es无法区分一个含有3个words的field和一个含有5个word的field。
如果改变field-level index-time boost值,需要reindex,而query -time 则不需要
如果一个使用index-time boost的field是multi_field,则这个boost会multipled,相当于增加了这个field的weight。
因此query time boost 是更好的选择。
3:query time boostinges提供的query都提供了boost参数用来影响相关性。boost值为2并不意味着最终的score是boost值为1的两倍score的决定因素很多,但是至少意味着重要程度是两倍。究竟如何设置boost的值没有一个通用的公式,常常需要try-it-and-see。
当在多个index中查询时,可以使用index_boost类提升整个索引的相关性权重。
这些权重对应lucene's pratial scoring function中的t.getBoost()。
如果你从explain的输出来看是发现不了t.getBoost()的,原因是这个值跟queryNorm柔和到一起了。
4:manipulating relevance with query structure
es的query DSL是相当灵活的,你可以把一个query clause从查询结构中上下移动,来实现相关性的调整。例如这个bool查询 quick OR brown OR red OR fox
正常情况下没一个query clause权重都是一样的,位于同一个level上。你会发现 brown 和 red 都是颜色,或许翻译成这样呢? quick OR (brown OR red) OR fox.
含义是一样的,但是相关性是有调整的,(brown or red)作为一个整体和quick fox 成了同一个level了。
5:not quite not
一个针对关键词apple的查询,可能的返回结构会包括苹果公司,水果,菜谱等等。其实我们想要的只是苹果公司的信息,那我们包装一个bool query,把含有水果信息没菜谱信息的doc过滤掉,剩下的就是我们的信息了么?我们采用了must_not的逻辑来处理这件事情是不是太苛刻了,谁规定难道苹果公司的doc中就不能包含菜谱,不能包括水果信息呢?那应该如何处理这种情况??
boosting query。是的,这是es提供的解决方案。
boosting query仍然允许最终结果包含水果呀菜谱呀之类的doc,但是downgrade了,rank them lower 。boosting query提供了positive query + negetive query。只有match到positive query的doc才会出现在结果列表中,但是如果doc也match到了negative query将会降级,有一个negative_boost参数来调整,当然这个参数要小于1.0.因此上边的问题就可以这样解决:postive query采用的text为apple,negative query采用的text为recipe等等。
6:ignoring tf/idf
有些情形下我们并不需要关心tf/idf,我们只是想知道某一个word是否出现。
比如我们查询wifi garden pool。来匹配一个酒店。我们只需要酒店具有这些设施,而出现多少次无所谓。如果一个term出现了,score就为1,否则就为0,出现多少次无所谓。
constant_score query:
这种查询可以包裹一个query或者一个filter,match到的doc的score都赋值为1,不考虑tf/idf。因此以上查询用一个bool query即可,
"bool" : {
"should" :[
{"constant_score" : { "query" : { "match" : { "description" : "wifi"}}}},
{"constant_score" : { "query" : { "match" : { "description" : "garden"}}}},
{"constant_score" : { "query" : { "match" : { "description" : "pool"}}}},
]
}
也许并不是所有的feature都是同等重要,可能pool更重要些,如何呢?只需要提升pool的boost即可。
其实,每一个query的feature都应该当作一个filter。doc要么含有这个feature要么不含有,这是filter的自然特性。而且,如果我们用filter的话,还可以caching。这么合适为什么不直接使用filter?问题来了:filter不能score。我们要做的就是在减小query和filter之间的gap,如何解决?请看function_score。
7:function_score query
function_score query 是我们控制score process的终极工具。他允许我们提供一个function来更改甚至替代socre过程。事实上,你也可以配合filter在不同的子集中使用不同的function。es内置了一些function:weight,field_value_factor,random_score,script_score等。下边将会给出一些使用的方式和例子。
8:boosting by popularity
设想我们有一个web的所有主题贴,用户可以根据自己的兴趣给这些帖子投票。因此我们在保持搜索相关性的前提下,想让投票数目高的排序在前。
我们可以通过function_score query轻松实现:
{ "query": { "function_score": { "query": { "multi_match": { "query": "popularity", "fields": [ "title", "content" ] } }, "field_value_factor": { "field": "votes" } } } }query首先执行,得到相关的帖子,filel_value_factor参数改变了最终的score为:new_score = old_score * num_of_votes.可见这是一种线性的改变。也可以采用其他的function让这种关系变的平滑,比如:new_score = old_score * log(1 + num_of_votes),设置modifier即可,如下:
"field_value_factor": { "field": "votes", "modifier": "log1p" }还可以进一步设定影响因子:new_score = old_score * log(1 + factor * num_of_votes)
"field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 2 }以上都是old_score到new_score的转换都是multiple的关系,或许这种影响太大,我们也可以修改为sum/min/max/replace等。
"field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 0.1 }, "boost_mode": "sum"
进一步可以设置funtion的最大影响数值max_boost。
"field_value_factor": { "field": "votes", "modifier": "log1p", "factor": 0.1 }, "boost_mode": "sum", "max_boost": 1.5不管field_value_factor最终计算结果是多少,1.5是上限(function的上限,而不是最终score的上限)!
9:boosting filtered subsets
8中的例子用一个function作用在所有的记录中,现在,我们想要把结果用filter划分为各个子集,没一个子集对应一个feature,并且应用一个function。
下边例子中运用的weight这个函数来提升score,类似于query中使用的boost参数。区别是:weight不需要normalize。
"query": { "function_score": { "filter": { "term": { "city": "Barcelona" } }, "functions": [ { "filter": { "term": { "features": "wifi" }}, "weight": 1 }, { "filter": { "term": { "features": "garden" }}, "weight": 1 }, { "filter": { "term": { "features": "pool" }}, "weight": 2 } ], "score_mode": "sum", } }可见poll这个feature的重要性要强于其他feature。
上边出现了一些新特性。
filter vs query:这个例子中我们并不需要全文搜索,只需要找到City为Barcelona的doc,返回记录的score=1.function_score query接受query或者filter,默认使用match_all query。
functions:出现了一系列的funtion,每一个function针对一个filter,配置的weight说明了相对重要性。
score_mode:支持多种方式,sum multiple avg max min first。
如果result doc没有match到任何一个filter,则保持score为1.
10:random scoring
这个是用来做什么的呢?在查询过程中,得分较高的排在最前边,也许非常适合的只是少数,大多数查询的score位于同一个level上。假设最高得分为5分的doc很少,但是为2-3分的结果很多,如果采用以上的方法,2-3的doc的返回结果顺序都是一样的。对于站点来说,想要给广告商更多的可能性,因此在同意level的站点,需要增加一些随机性,不要每次都一样,这样对站点也是公平的。但是对同一个用户来说,我们想让他看到的是一致的情况,对不同用户是随机的。为了满足这种需求random score出现了。
"function_score": { "filter": { "term": { "city": "Barcelona" } }, "functions": [ { "filter": { "term": { "features": "wifi" }}, "weight": 1 }, { "filter": { "term": { "features": "garden" }}, "weight": 1 }, { "filter": { "term": { "features": "pool" }}, "weight": 2 }, { "random_score": { "seed": "the users session id" } } ], "score_mode": "sum", } }上例中random score并没有任何filter,所以将会应用与上边所有的filter。
选用了用户id作为seed,保证了同一个用户看到的情形是相同的。
11: the closer the better
更加的精细的得分机制,满足某一个特定范围的条件以及不在这个范围之内的doc是如何采用function score进行得分衰退的,linear?exp?gauss?
12:scoring with script
如果以上function score仍然不能满足需求,则采用script编写评分机制即可,完全定制。
script_score提供了很大的灵活性,使用脚本你可以访问任何field,甚至是tf idf等。但是script会有性能损耗。如果你发现你的script不够快,有三个建议:
(1)尽量提前计算一些信息存储在doc中
(2)内置的groovy是比较快的,但是还是没有java快,所以你可以用javascript代替groovy
(3)使用rescore,将script应用到最佳的doc上(best scoring documents)
13:pluggable similarity algorithms
es默认使用的相似性算法是Lucene's Pratical Scoring Function.
同样支持其他的:okapi bm25等。
14:changing similarities
相似性算法可以应用到没一个field上,只需要在mapping中设置即可:
"title": { "type": "string", "similarity": "BM25" },15:relevance tuning is the last 10%
以上只是一些方法,关键还是要在应用中逐步寻找最佳的相关度。
monitor你提供出来的doc,用户的点击情况,前几条记录的点击情况,这些都是优化的线索。