elasticsearch---search in depth之controlling relevance

数据库系统主要处理结构化数据,他们只是去查询一条记录在不在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 boosting

es提供的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,用户的点击情况,前几条记录的点击情况,这些都是优化的线索。






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值