elasticsearch评分进阶

来源:互联网 发布:表格删除重复数据 编辑:程序博客网 时间:2024/05/19 07:10

elasticsearch 评分进阶

原文引用自:Advanced Scoring in elasticsearch,作者还有一篇Elasticsearch评分的经验说明,建议爬墙参考slideshare上的资源对照查看。
如有侵权请联系:shinehiy@foxmail.com


之前关于elasticsearch的文章中,提到并解释了Lucene的内建评分算法的机制。也简要的提到了通过给不同的文档的field或query的term来影响最终的评分算法。本文会针对boost进行详细的说明。

为什么要Boost

我开始做评分的时候的有个问题:到底为什么要boost?Lucene的评分算法到底靠不靠谱?我在索引里面放的测试文档比较少的时候效果貌似还不错,但是加了更多的文档,结果就不怎么好了,这个时候我意识到boost的必要性了。Lucene的评分算法在通用的情况下效果较好,但是并不是针对你的特定领域或应用设计的。Boost可以让你对不同的文档类型进行调节,增加领域特定的逻辑,并融入附加的标记。

在给出特定例子之前,先说明下我遇到的搜索应用的情境。
该应用为IGN提供搜索服务,IGN主打:”游戏、娱乐、一切享乐“。我们需要从后端API里面索引下列四种类型的内容:1. 文章;2. 视频; 3. wiki网页;4. ”对象“(游戏、电影、各种秀等等)。默认的,所有类型的结果都以list集合的形式返回。

为不同的文档type进行修正

如果你的文档是同质的话,Lucene评分算法是不错的。但是如果你有不同的文档类型,你需要做一些手工的调整。比如,我们需要索引文档和视频。文档中有很多文字性的内容——文档的body——但是对于视频,只有简短的描述。默认的lucene更偏向于匹配较短的field,所以匹配结果视频的得分往往比得分要高。

因为elasticsearch支持跨多个索引进行搜索,通过为每个type创建分开的索引、并进行跨索引query的查询,对不同的文档type进行调整是可行的。我没有进行测试,但是我觉得对每个query都应该用coordination因素进行标准化,那么一个高分的视频会得到和一个高分文章相同的权重。然而,该方法仍然会独立的考虑每个内容type中的term频率,我并不确定这会如何影响结果。

给文章一个较小的boost值是较简单的方案,特别是我们准备我们因为各种独立的因素会控制不同内容type的重要性。

增加特定领域逻辑

有事,你会有一个特定领域的逻辑Lucene难以识别。比如,我们的评论文章可能是我们网站的内容中最重要的type。因为我们的用户就是来看评论的,我们给评论文章一个小boost值,让他们比其他文章得分高一些。

另外一个例子是树状的wiki页面。视频和对象都有比较短的文本描述。文章通常要长一些,虽然有时候也有一些短文章影响到新闻或者提高一些其他的内容,短文本还好。然而,一个短wiki页面经常是一组树状的标记,所以会比其他结果的得分要低。这与lucene本身的特性相反,Lucene更偏向于匹配短的wiki页面,给出较高的得分。

合并附加标记

大多数情况,我们网站内容的一个关键要素就是文章内容应该随时间而衰减。举个例子,一个刚发布的游戏的评论在本周内非常重要,但是下个月就不那么重要了,一年之后更不用说了。lucene在打分算法中并没有考虑到内容的新颖性/时效性这类要素。但是如果如果时间要素在你的领域中起着关键作用,你就会对其进行boost*(如何进行时间要素的boost在本文后续详解)*。

如果针对一些文章、视频的评论和内容很多,我们就会对游戏、电影和TV show这些对象进行boost。换个更简单的例子就是,销量大的产品应该被boost,或者有更多评论和点击阅读的文章应该被boost。你的特定领域哪个属性更重要,你就应该动手给他进行boost。

Index阶段Boost vs. Query阶段Boost

在对文档进行索引时可以进行boost,也可以在query阶段进行搜索的时候进行boost。如果一篇文档总是比其他的更重要,你应该考虑在进行索引的时候就对这些文档进行boost。预先进行过boost的文档搜索更快,因为在进行搜索时要做的事更少。但是即使你知道一些文档总是比其他的重要,但是你不确定到底有多重要。如果boost太大,如果匹配上了,文档就总在搜索结果的最上面。如果boost因素太小,重要的文档就没法从其他文档中凸显出来。

如果在索引阶段就进行boost,如果你要换boost的值就得重新建一次索引。除非你手工向索引中添加文档,一个一个决定这个文档该给啥boost值,你得用脚本或者程序来建立索引,用一些逻辑或者规则来决定boost。逻辑或规则的改变会影响很多文档,要让逻辑或规则生效,就有得重新进行索引。如果你的索引比较小,那还可以。我们的索引重建一次要好几个小时,所以我们会尽可能不在索引阶段就进行boost。在query阶段使用boost可以随意增加boost,改变boost的标准,随时改变boost的长度。比起运行时的一些额外开销,灵活性是值得的。

即使你在索引阶段和query阶段进行了boost,有一些boost是不得不在query阶段做的,有一些boost是在query阶段才生效,因为在索引阶段无法获得足够的信息。比如,如果你要基于文档的新鲜度(该文档的时间戳时候离当前时间较近),当前时间(索引的当前时刻)就无法从索引阶段获知。这个例子中,如果你经常重建索引,你可以把index时间作为当前时间,这样就可以避免query阶段的boost了。

应用Boost

基本上每一个elsaticsearch的query类型都有boost参数让你可以针对这个query进行调优,但是我们没有使用这个query因为就只有一个主query。主query是query_string的query,对用户的query进行分词,找到匹配项,用Lucene的默认打分算法进行打分。下面我们用一些boost来决定是否符合特定的标准。

在早先的原型中,我通过对主query进行custom_score的包裹完成了boost。顾名思义, custom_scorequery允许你使用定制的逻辑来计算每个文档的得分,可以通过script传入参数。默认的,脚本集成的是MVEL,但是也支持其他语言。你可以通过指定的_score变量来操作得分,所以开始的时候我是这样做的:

{  "query": {    "custom_score": {      "query": { ...the main query... },      "script": "_score * (doc['class'].value == 'review' ? 1.2 : 1)"    }  }}

确实生效了,但是量大了就不那么好使。随着我增加越来越多的boost,得用好几行才能结束这个表达式。每个文档的field都需要在索引阶段存储,这样脚本才能检索到并操作这个值,这样索引变得很大操作也变慢了。幸运的是,我们可以使用更好的工具完成这个操作,custom_filters_scorequery。文档中提到:

ustom_filters_scorequery允许执行一个query,并且如果命中的结果匹配到了提供的filter(按顺序),就使用boost或是脚本来计算该值。

可以明显简化,并提高基于参数化打分的效率,因为这些filter可以进行缓存提供较高的性能,从而boost/脚本也更简单了。

转成custom_filters_scorequery,上面的例子就变成了这样:

{  "query": {    "custom_filters_score": {      "query": { ...the main query... },      "filters": [        {          "filter": {            "term": {              "class": "review"            }          },          "boost": 1.2        }      ]    }  }}

如果你想增加boost,就再添一个filter指定其标准,并赋boost。可以使用任何的filter,一个包裹另一个的filter甚至andfilter。如果你有多个filter,那么需要指定这多个匹配filter如何使用score_mode进行融合。默认的,是使用第一个匹配filter的boost,但是如果你有好多个filter都匹配上了你可以设置score_mode,比如设置multiply 应用到所有的boost上。

如下的query boost评论提升了20%,boost文章提升20%(所以评论的文章会boost 44%)(译者注: (1 + 20%)*(1 + 20%)),然后对wiki页面进行惩罚,小于600词长的会降权到80%。

{  "query": {    "custom_filters_score": {      "query": { ...the main query... },      "filters": [        {          "filter": {            "term": {              "class": "review"            }          },          "boost": 1.2        },        {          "filter": {            "term": {              "type": "article"            }          },          "boost": 1.2        },        {          "filter": {            "and": [              {                "term": {                  "type": "page"                }              },              {                "range": {                  "descriptionLength": {                    "to": 600                  }                }              }            ]          },          "boost": 0.2        }      ],      "score_mode": "multiply"    }  }}

变量Boost

有时你会想根据一个document里面的field的boost来调节boost。比如,如果你想boost最近的文档,今天发布的文档就会比昨天发布的文档增加boost,而昨天发布的文档比上周发布的文档更大。即使filter会进行缓存,而且跑起来相对比较快。给今天发的文章给个boost,给昨天发布的再给个boost,上周的文章在给个boost一点都不实用。幸运的是,custom_filters_score 的query可以接受一个script ,这些情况下就不用再使用boost了。

在Boosting Documents in Solr by Recency, Popularity and Personal Preferences(打不开请挂代理,来自slideshare),Timonthy Potter谈到了solr的recip 函数来计算最近文档的boost值。不幸的是,elasticsearch并没有提供recip函数,但是你可以很简单的就实现整个函数y = a / (m * x + b),并转成script函数用到elasticseach里面来。

在下面的例子中,我用了值m, a 还有b按照slide中第七页的设置:m = 3.16E-11, a = 0.08b = 0.05。因为我们索引中有些未来时间的日期,所以我加了个绝对值函数abs()来计算query时间和文档的时间戳的绝对值。我是对boost值设置为1,即设置一个新鲜度的值,而不是一个衰减值。

{  "query": {    "custom_filters_score": {      "query": { ...the main query... },      "params": {        "now": ...current time when query is run, expressed as milliseconds since the epoch...      },      "filters": [        {          "filter": {            "exists": {              "field": "date"            }          },          "script": "(0.08 / ((3.16*pow(10,-11)) * abs(now - doc['date'].date.getMillis()) + 0.05)) + 1.0"        }      ]    }  }}

有了这个值,当前的文档会被加权到160%(boost值2.6)。这个值在10天后会掉到100%,一个月后掉到60%,半年后掉到15%,一年后掉到8%,2年后掉到4%。(可见图)

最后要说明的是,elasticsearch的script进行缓存会执行效率更高,把那些随着query改变的参数通过params值传入,不要把他作为string直接插入到脚本中。这样,脚本就变成静态值了,无法缓存,但是用参数就不会出现这样的情况。


精选答疑:
Nick (June 12, 2013 at 11:34 am)
在跑主query时我遇到了如下问题,我把post_date作为一个field放在了索引里面,存储时设置为了string类型。除了如下错误:

“error” : “ElasticSearchException[Couldn’t parse query from source.]; nested: ElasticSearchParseException[failed to parse date field [link], tried both date format [YYYY-MM-dd HH:mm:ss], and timestamp number]; nested: IllegalArgumentException[Invalid format: \”link\”]; “,

貌似是elasticsearch无法把string转成date。
我从field中移除了post_date,貌似这个script还是不可用,报错如下:

error” : “CompileException[[Error: No field found for [org.elasticsearch.index.fielddata.ScriptDocValues$Longs@33de6c02] in mapping with types [post]]\n[Near : {… *pow(10,-11)) * abs(now – doc[post_date].date.getM ….}]\n ^\n[Line: 1, Column: 35]]; nested: ElasticSearchIllegalArgumentException[No field found for [org.elasticsearch.index.fielddata.ScriptDocValues$Longs@33de6c02] in mapping with types [post]]; “,

我用的curl如下:github链接

作者回复:
第一个报错貌似是elasticsearch在处理date的field时候遇到了链接,你可以检索下索引里面的文档,验证下date field是不是有链接。

第二个不确定,如果文档中没有这个field不应该拿来跑,所以我不太明白这里的”no field found”

提问者Nick:
问题解决了,我在post_date里面用了单引号,所以curl也得用单引号来包裹整个request。我改成双引号就能用了。

其他问答:
1.‘boost’问题,尝试’boost_factor’
2. ES 1.0+不再支持custom_script,可以考虑使用gauss过滤器:

{    “query”: {        “function_score”:         {            “query”: {             }, // your query            “functions”:             [                {                    “gauss”: {                        “date”:                         {                            “origin”: “now/d”, // using now/d should help with caching if you’re using several days or weeks for scale and often, if you want decay during a single day (ie trending) use now                            “scale”: “50w”, // how long to get to decay (for trending this should be something like 1d)                            “offset”: “4w”, // how long doesn’t have any affect (for trending this might be 4h)                            “decay”: “0.5// after 1 unit of scale the relevance will have decayed by this much (for trending you might want this as 0.3)                        }                    }                }               ]        }    }}
0 0