实时动态排序列表设计优化思路

刚好接手了项目的服务列表及排序重构的任务。

我们的项目做的属于撮合服务,类似于滴滴。有服务者,也有客户。服务者在我们平台提供服务,顾客可以预约并下单,服务者会上门服务。所以,把合适的服务者展示给客户就显得非常关键。同时,服务者对自己在客户界面列表的排序也非常重视。

我们业务是上门服务,有很强的lbs特点,所以之前的逻辑也就很简单了,装了个postgresql,根据客户位置和活动半径,再加上日期,评分等其他条件,构建一条sql语句,就可以出结果了。但是因为涉及连多表查询和复杂的排序规则,导致查询非常慢。

和传统的搜索排序和电商的排序相比,服务平台的服务者排序有着特殊的需求,需要考虑的因素包括:

  • 距离因素
  • 时间和相对距离
  • 质量和评价

注意了,那个相对距离,意思是在某个服务时间段,服务者在什么位置。比如一个服务者住在回龙观,但是他明天上午9点到10点在上地服务一个顾客。那么你如果根据时间段查询服务者,那么这个服务者应该算在上地,而不是回龙观。至于什么时候取常住地,什么时候取当前位置,是根据运营策略定的,我们会根据上一份订单结束时间1小时内来判断查询位置。

总结下就可以发现,可以分两类,一类是绝对属性,一类是相对属性。对于绝对属性,我们可以把他当做一个查询条件。但是相对属性需要两两计算,这就不容易处理了。那么我们如何用简单又高效的方案来处理这个问题呢?

我们知道,数据的查询基本都是如下步骤

query -> filter -> score ->sort -> slice

其中,

query是获取候选集,query的效率直接决定了查询总效率。

filter并不是必须的,是为了过滤掉无用的数据。

score可以是固定的字段值,也可以是计算后的值,如果是计算值,一般会配合filter来操作。

所以,我的工作只要是从query,filter,score这三个方面展开。

query 要提高效率,qurey扫描的行数和返回的行数一定要尽可能的少。如果是用数据库,则一定要走区分度高的索引。在位置,日期,评分,报价...这些属性里区分度最高的就是位置了。

geo的查询有成熟的工具,postgresql和es都可以,但是这种方案无法有效的利用缓存,为此我才用了另一种方案。

我们将地图分成1km x 1km的小格子,每个格子建立倒排索引。其实有个成熟的划分方案geohash,但是该方案优势是可以支持不同精度的计算,然而这一点我们是不需要的,为此我们才有了一个更简便的方案,把经纬度直接换算成长度坐标,然后在坐标中画格子。这里有点要注意,地球是球体,我们在北纬30度左右,所以要特别换算下。

地球半径:6371000M
地球周长:2 * 6371000M  * π = 40030173
纬度30°处地球周长:40030173 * cos30° = 34667146M
经度(东西方向)1°实际距离 111194.925M
北纬30°(南北方向)1°实际距离:87622.794M

我们拿到一个经纬度后,可以换算成长度坐标,然后横纵坐标除以1000,就可以算出对应的格子。

在查询的时候,会判断查询点在哪个格子,然后会算出周围的格子,分别取出服务者id,就是filter和score了。

filter和score在filter之前,为了提高效率,我们可能会先批量取出相关的服务者信息,所以实际上filter的对象不是List[servicer_id],而是List[servicer_info]。为了提高效率,我们会先遍历id结合,然后通过mget批量取信息。filter和score虽然是两个步骤,但是在执行层面是合二为一。整个filter的过程不仅排除了不符合规则的数据,同时还给每项计算量分值。

打分公式是score= a1*func1(servicer_info) + a2*func2(servicer_info) + a3*func3(servicer_info)......

每个函数表示一个维度规则,前面的a系数表示的是该维度的权重。根据运营策略和场景的不同,每个维度的权重不同,前端页面的XX优先也是通过提高维度的权重来实现的。注意的是,score是一个相对分数,并没有上限。

sort和slice 到这里就简单了,根据score从大到小排序,然后分页输出。

上述方案里,我是将位置信息当做了query条件,这是因为地理位置区分度最高,我们在10公里范围内的合适的人不会超过100人。具体选哪个应该根据实际情况来判断。但如果多个条件都差不多,而且查出的数据都过多,那么这个时候要么用联合索引(不等同于数据库的联合索引),那么起一个单独的排序服务,比如es就算是一个这样的服务。