elasticsearch之实例篇

本文上接《elasticsearch之搭建篇》,看看如何实现类似糯米的检索功能。

在商铺和商品的存储方面,有嵌套(Nested)和父子文档(Parent-Child)两种方式,下面将依次探索。

嵌套(Nested)

创建type

商品将嵌套在商铺的文档中,其创建方法如下:

可见,商品列表作为一个属性存储在商铺中(type=nested,嵌套的),一个商铺有多个商品。(嵌套文档可以在这里学习

通过curl查看刚刚建立的商铺type:

上述信息与我创建时传入的一致,表明:商铺type已经成功建立,它利用嵌套文档技术,在其内部直接保存商品列表。

录入数据

我们通过bulk API批量的插入测试数据:

  • 商铺ID作为ES文档的唯一_id。
  • 商品ID作为普通字段保存在嵌套文档的product_id字段。

嵌套查询

因为商铺和商品是嵌套关系,所以在查询时需要使用”嵌套查询”语法。

我的查找需求表达如下:

若某商铺的名称或者其售卖的”商品”的名称,能够匹配”搜索关键字”,那么返回该商铺的信息。

分析一下这个查询的组成部分(注意配合代码注释理解):

  • query:查询语句必须放在其内部。
  • bool:组合查询,可以表达多个子句之间的AND(must),OR(should),NOT(must_not)关系。
  • should:OR的意思,里面多个子句满足任意一个即匹配,这个例子有2个子句。
    • should的总相关性是这样计算的:所有子句的相关性和/子句的数量。
  • match:全文匹配,会对$keyword分词,然后分别进行倒排查找。
    • 该查询是should的第一个子句,会匹配得到一个相关性。
  • nested:嵌套查询,是should的第二个子句。
  • path:嵌套查询指向的子文档路径。
  • score_mode:嵌套文档有多个,该参数指定nested子句的总相关性是如何计算的。
    • 这里指定max,表示取多个商品中的最大相关性。
  • query:对于嵌套查询来说,查询语句必须放在其内部。
  • match:全文匹配,会对$keyword分词,然后分别进行倒排查找。
    • 该查询是nested query的唯一子句,产生的相关性是nested的总相关性,也是should第二个子句的相关性。

其结果如下:

查询结果出乎意料!

直观来看,”东方宫兰州拉面”应该更符合我的预期,为什么”鑫明明拉面”的相关性却高于”东方宫兰州拉面”呢?

首先,我们要了解相关性,它表示搜索词与文档的匹配程度。

搜索引擎一般采用『词频/逆向文档频率 (TF/IDF)』来计算相关性:

  • TF:单词(TERM)在一个文档内的出现比例,出现越多说明TERM对这篇文章更重要,比如本文的”ES”就多次出现。
  • IDF:单词(TERM)在所有文档中出现的比例,出现越多说明TERM越大众越不起眼,比如”的,了,吧”这些助词。
  • TF/IDF:TERM在文档内的TF越高,在所有文档中的IDF越小,说明TERM与该文档越相关。

通过肉眼分析,”东方宫兰州拉面”这家店无论从名字还是商品的名字都更贴近于我的搜索词”东方宫拉面”,是ES有BUG吗?

答案:并不是,出现这个现象的原因是因为不准确的IDF!

我的商铺表有3个分片Shard,通过查看获知”东方宫兰州拉面”独自在分片1中,而”鑫明明拉面”和”开海饭店”在分片2中。

ES在计算IDF的时候是基于分片内的数据统计的,分片1内的”拉面”只出现在”东方宫兰州拉面”内,相当于100%的IDF(在所有文档内出现);分片2内的”拉面”只出现在”鑫明明拉面”内,而”开海饭店”里并没有出现,相当于50%的IDF(在1/2的文档内出现),讲到这里我们就明白了:”东方宫兰州拉面”和”鑫明明拉面”中”拉面”都出现了1次,但是前者的IDF是1,而后者是1/2,经过TF/IDF计算显然是后者的值更大,也就是更相关了!

这个问题在数据规模较大的情况下可以忽略,在我们开发阶段可以通过指定一个参数解决:search_type=dfs_query_then_fetch,它将获取集群所有分片的IDF和之后再计算TF/IDF,因此更加准确。

 

这次结果正确:

最佳子句

接下来我替换搜索关键字为:”兰炒饭”,其实我本意是”兰州炒饭”,只不过我输错了(如果搜索仍旧可以给我理想的结果,我会爱上它)。

我们直接看查询结果:

从直接上来看,”开海饭店”应排在第一位,因为它正在售卖我的最爱:”兰州炒饭”,可为什么”东方宫兰州拉面”在第一位呢?

之前说过,bool的should会对其内部的2个子句(一个匹配商铺名称,一个匹配商品名称)的相关性加和并除以子句个数(这里有2个子句),其结果作为商铺文档的总相关性。

  • “开海饭店”是商铺标题,和”兰炒饭”没有一丁点相关性,因此第一个子句的相关性=0。
  • “兰州炒饭”完美匹配我的查询,因此第二个子句有很高的相关性。
  • 总相关性 = (0 + 一个很高的相关性)/ 2,变成了很高相关性的一半。

 

  • “东方宫兰州拉面”是商铺标题,出现了”兰”,因此第一个子句的相关性还不错。
  • “蛋炒饭”出现了”炒饭”,因此第二个子句的相关性还不错。
  • 总相关性 = (一个不错的相关性 + 一个不错的相关性)/2,还是一个不错的相关性。

上面的查询结果就是这样的情况下产生的,完全不符合预期!

我的初衷是找到最符合搜索关键字的字段,无论它是”商铺名称”还是”商品名称”。

最佳字段“就是解决这个问题的:它保留多个检索字段中最大的相关性作为总相关性,查询变化如下:

主要做了如下调整:

  • bool组合查询替换成了dis_max最佳字段查询。
  • should替换成了queries,下面同样包含多个查询子句。

现在结果正确!

过滤距离

通常,我们希望找到附近N公里内的商铺,因此必须利用坐标进行筛选,ES提供了索引地理位置的能力。

假设我的坐标是(X,Y),搜索范围是以它为圆心,半径为1公里的圆形,那么ES会怎么做呢?

ES首先为每个商铺的merchant_location建立了索引,(X,Y)坐标将被建立2个索引:

  • 按经度索引。
  • 按纬度索引。

ES在执行查询时,首先以(X,Y)为中心画一个矩形,它恰好能够包裹圆形,这样的目的是可以利用2个索引快速缩小范围:

  1. 矩形的x轴区间范围,可以使用经度索引筛选出一批X轴在1公里范围内的文档。
  2. 矩形的y轴区间范围,可以使用纬度索引筛选出一批Y轴在1公里范围内的文档。
  3. 两个文档集合求交集,可以得到矩形范围内的所有文档。
  4. 矩形比圆形要多一些区域,因此遍历所有文档计算它们和(X,Y)之间的距离,将圆形外的点删除。

这种工作方式叫做”地理坐标盒模型“,它是一种精度高,计算耗费资源比较多的一种手段(另外一种精度低,资源耗费少的方式是geohash)。

下面的请求,首先利用”距离”过滤出1KM内的商铺,之后再基于过滤的结果进行全文检索并计算相关性:

这里使用了过滤,它在全文检索之前对数据进行按条件筛选,过滤的结果可以被ES缓存。

在ES5.x版本中,过滤语法filter必须和全文检索放在一个bool组合中,而全文检索放在must中即可。

下面是结果,它们按相关性排序:

现在扩大距离范围distance为2km,可以看到三个”商铺”全部返回(我就不贴结果了,亲自动手试试吧)。

排序

在糯米搜索中,”综合排序”其实就是指相关性排序,是ES的默认排序方法。

但是仔细观察糯米检索会发现,它支持若干其他排序方式,比如:按距离排序。

新的查询需求如下:搜索2KM之内,与”拉面”相关的”店铺”,并且按照距离远近排序。

  • sort是必须要写的。
  • sort内部可以并列多个排序条件。
  • _geo_distance是坐标排序的系统关键字。

结果如下:

更多的过滤和排序

上面的几个例子里包含了这些知识点:相关性排序,距离排序,距离过滤…

为了体现ES的强大之处,我们继续丰富搜索请求:

2KM以内,与”拉面”相关,商铺评分>=4分,商铺均价<=20元,按商铺评分、商品总销量排序,并且返回结果中包含距离。