ElasticSearch 之 dis_max tie_break的应用
1. 插入数据
PUT /my_index/my_type/1 { "title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen." } PUT /my_index/my_type/2 { "title": "Keeping pets healthy", "body": "My quick brown fox eats rabbits on a regular basis." }
2. 搜索
{ "query": { "bool": { "should": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } }
3. 结果
{ "hits": [ { "_id": "1", "_score": 0.14809652, "_source": { "title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen." } }, { "_id": "2", "_score": 0.09256032, "_source": { "title": "Keeping pets healthy", "body": "My quick brown fox eats rabbits on a regular basis." } } ] }
理论上 2 的文档其实是我们需要的,但是搜索的结果不是。要理解原因,需要知道 bool 查询时如何计算得到其分值的:
- 运行
should
子句中的两个查询 - 相加查询返回的分值
- 将相加得到的分值乘以匹配的查询子句的数量
- 除以总的查询子句的数量
文档1 在两个字段中都包含了 brown,因此两个 match 查询都匹配成功并拥有了一个分值。文档2 在 body 字段中包含了 brown 以及 fox,但是在 title 字段中没有出现任何搜索的单词。因此对 body 字段查询得到的高分加上对 title字段查询得到的零分,然后再乘以匹配的查询子句数量1,最后除以总的查询子句数量2,导致整体分值比文档1 的低。具体计算过程如下:
score_1 = (1 + 1) * 2 / 2 = 2 score_2 = (0 + 2) * 1 / 2 = 1
在这个例子中,title 和 body 字段是互相竞争的。我们想要找到一个最佳匹配(Best-matching)的字段。
如果我们不是合并来自每个字段的分值,而是使用最佳匹配字段的分值作为整个查询的整体分值呢?这就会让包含有我们寻找的两个单词的字段有更高的权重,而不是在不同的字段中重复出现的相同单词。
4. dis_max 查询
相比使用bool查询,我们可以使用dis_max查询(Disjuction Max Query)。Disjuction的意思"OR"(而Conjunction的意思是"AND"),因此Disjuction Max Query的意思就是返回匹配了任何查询的文档,并且分值是产生了最佳匹配的查询所对应的分值:
{ "query": { "dis_max": { "queries": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } }
它会产生我们期望的结果:
{ "hits": [ { "_id": "2", "_score": 0.21509302, "_source": { "title": "Keeping pets healthy", "body": "My quick brown fox eats rabbits on a regular basis." } }, { "_id": "1", "_score": 0.12713557, "_source": { "title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen." } } ] }
5. 最佳字段查询调优
如果用户搜索的是"quick pets",那么会发生什么呢?两份文档都包含了单词 quick
,但是只有文档2 包含了单词 pets
。两份文档都没能在一个字段中同时包含搜索的两个单词。
一个像下面那样的简单 dis_max
查询会选择出拥有最佳匹配字段的查询子句,而忽略其他的查询子句:
查询语句: { "query": { "dis_max": { "queries": [ { "match": { "title": "Quick pets" }}, { "match": { "body": "Quick pets" }} ] } } } 得到的结果: { "hits": [ { "_id": "1", "_score": 0.12713557, "_source": { "title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen." } }, { "_id": "2", "_score": 0.12713557, "_source": { "title": "Keeping pets healthy", "body": "My quick brown fox eats rabbits on a regular basis." } } ] }
可以发现,两份文档的分值是一模一样的。
我们期望的是同时匹配了title
字段 和 body
字段 的文档能够拥有更高的排名,但是结果并非如此。需要记住:dis_max
查询只是简单的使用最佳匹配查询子句得到的_score
。
6. tie_breaker
但是,将其它匹配的查询子句考虑进来也是可能的。通过指定tie_breaker
参数:
查询语句: { "query": { "dis_max": { "queries": [ { "match": { "title": "Quick pets" }}, { "match": { "body": "Quick pets" }} ], "tie_breaker": 0.3 } } } 它会返回以下结果: { "hits": [ { "_id": "2", "_score": 0.14757764, "_source": { "title": "Keeping pets healthy", "body": "My quick brown fox eats rabbits on a regular basis." } }, { "_id": "1", "_score": 0.124275915, "_source": { "title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen." } } ] }
现在文档2 的分值比文档1 稍高一些。
tie_breaker参数会让dis_max查询的行为更像是dis_max和bool的一种折中。它会通过下面的方式改变分值计算过程:
1. 取得最佳匹配查询子句的_score。
2. 将其它每个匹配的子句的分值乘以tie_breaker。
3. 将以上得到的分值进行累加并规范化。
通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。