基于 Google-S2 的地理相册服务实现及应用
马蜂窝技术原创内容,更多干货请关注公众号:mfwtech
随着智能手机存储容量的增大,以及相册备份技术的普及,我们可以随时随地用手机影像记录生活,在手机中存储几千张甚至上万张照片已经是很常见的事情。但另一方面,当我们想从这么多张照片中去找到一张,也是一件麻烦事。
马蜂窝作为旅行玩乐平台,希望实现「会玩的人」与「好玩的事」之间的连接。众多旅行爱好者在这里记录和分享他们的旅行记忆,使马蜂窝在旅游 ugc 领域累积了大量内容。因此,不断优化用户在发布内容时的体验是我们一直努力的主向。
用照片、视频记录旅行是最直接的方式。本文将介绍马蜂窝如何通过 app 地理相册空间索引的应用,为用户提供直观、好用的图片分享服务。
part.1 应用场景和需求
要想让用户快速地找到想要分享的照片/视频,我们需要一个有效且合理的筛选手段,对用户的相册进行聚合、排序,提升用户依托相册去分享和记录生活时易用性和便捷性。
首先要确定聚合排序的筛选维度。照片的地理位置就是最直观的分类维度;同时,记录最近发生的事情符合用户的发布行为习惯。因此我们方案要满足的需求是:
- 根据目的地和时间,对用户相册进行聚合、排序;
- 基于某个地理位置信息和给定范围,在用户相册中搜索给定范围的照片/视频。
本文提及的地理相册服务在马蜂窝 app 内主要有两个落地场景。
1.1 笔记
「笔记」是以图片、视频为主要呈现形式的旅行短内容分享。用户发布笔记的第一个环节就是从相册中选择需要发布的照片/视频,在新版 app 中,基于地理相册服务结合马蜂窝自有目的地数据,对用户相册进行按照地点维度的聚合分类,并且按照片/视频的创建时间由近及远的排序,提升用户选择发布效率。
1.2 足迹
「足迹」这一产品的功能,旨在帮助马蜂窝用户以自动同步或手动点选去过的国家和地区这种更简易的方式记录旅行。在「我的足迹」中有一个场景,会鼓励用户对去过的但还没有发布笔记的地点发布笔记。此时地理相册服务可以帮助用户发布相册中以指定地点为圆心,给定半径范围内的所有照片。
part.2 方案设计与算法选型
2.1 初期方案
初期我们想到的方案比较直观,也比较粗暴,就是对相册进行遍历后由服务端计算结果。具体来说,首先取出用户所有携带地理信息的照片/视频,然后将地理信息(经纬度)上传服务端,由服务端进行聚合和筛选,返回给客户端结果,但是这个方案有很多缺点。
文章开始我们已经描述了目前用户手机设备中的照片数量是成千上万的,如果遍历所有图片,这上传的数据体量是巨大的;同时,一般用户照片的地理位置会有很多呈现出成簇聚集的状态,因为一般我们会在一个地点范围内拍摄许多照片,这就导致了大量的重复聚类的计算。
如果要优化这个方案,针对第一个需求我们可以采用缓存+增量请求的方式,因为用户分类数据是稳定的。但是针对给定范围查询的需求,我们无法做缓存,这就需要每次都请求服务端做大量的计算,对于时间的消耗是不能容忍的。
可以看到,上述方案的挑战主要在于用户相册中地理信息的数据量和重复度、依赖服务端计算搜索结果导致的性能问题和用户体验。经过调研我们发现,基于地理空间点(经纬度)索引算法可以很好地解决这些问题。
2.2 基于地理空间点索引算法的实践
结合我们的实际需求来理解地理空间点索引算法,即找到合适的方法来对地理空间中海量的坐标点添加索引,从而对空间点进行快速查询和排序的一种算法。
我们对一些比较通用的地理空间点索引算法进行了选型比较,下面主要介绍 geohash 算法和 google-s2 算法。
2.2.1 算法选型
(1)geohash
geohash 算法即地理位置距离排序算法。geohash 是一种地理编码,由 gustavo niemeyer 发明。它利用一种分级的数据结构,把空间划分为网格。
geohash 属于空间填充曲线中的 z 阶曲线的实际应用。geohash 有一个和 z 阶曲线相关的性质,那就是一个点附近的地方 hash 字符串总是有公共前缀,并且公共前缀的长度越长,这两个点距离越近。由于这个特性,geohash 就常常被用来作为唯一标识符,比如在数据库里面可用 geohash 来唯一表示一个点。
geohash 这个公共前缀的特性就可以用来快速的进行邻近点的搜索。越接近的点通常和目标点的 geohash 字符串公共前缀越长。但是 z 阶曲线有一个比较严重的问题,就是它的突变性。在每个 z 字母的拐角,都有可能出现顺序的突变,导致搜索临近点的精确度较差,不能满足我们的业务场景对精确度的要求。
(2)s2 算法
s2 其实是来自几何数学中的一个数学符号 s²,它表示的是单位球。
s2 算法采用正方体投影的方式将地球展开,然后利用希尔伯特分形曲线将展开后的二维地球进行填充,完成了对三位地球的降维和分形,从而得到空间坐标点与希尔伯特分形曲线的函数关系,即将球面经纬度坐标转换成球面 xyz 坐标,再转换成正方体投影面上的坐标,最后变换成修正后的坐标在坐标系变换,映射到 [0,2^30^-1] 区间,最后一步就是把坐标系上的点都映射到希尔伯特曲线上。最终,映射到希尔伯特曲线上的点成为 cell id,即是空间坐标点的索引。
s2 的最大的优势在于精度高。geohash 有 12 级,从 5000km 到 3.7cm,中间每一级的变化比较大。有时候可能选择上一级会大很多,选择下一级又会小一些。而 s2 有 30 级,从 0.7cm² 到 85,000,000km²,中间每一级的变化都比较平缓,接近于 4 次方的曲线。所以选择精度时不会出现 geohash 选择困难的问题。
综上,s2 算法能够满足我们对于功能和精度上的要求,因此最终选择 s2 算法作为空间点索引算法的实现方案。
part.3 功能实现与性能优化
3.1 模块设计
本文中的 app 地理相册服务主要基于相册索引数据操作、用户相册扫描、相册索引服务和相册地点分类计算四大模块实现:
以下分别介绍。
3.1.1 相册索引数据操作模块
相册位置信息的索引采用数据库作为存储介质,将用户照片信息以及通过 s2 算法计算出来的 cell id 存储到数据库当中。其中,考量存储的数量和对搜索和聚合经度的要求,存储了从 level4~level16 经度级别的 cell id。
相册索引数据操作模块,由数据库(db)和数据库操作层(dao)组成。数据表的设计见下图:
数据库操作层(dao)封装了数据插入、删除、查询等基本操作的 api。
3.1.2 用户相册扫描模块
用户相册扫描模块基于 ios 原生提供的相册查询的 api,将用户相册的数据与本地数据库中存储的照片数据进行对比,提取出新增照片数据和用户已经删除的照片。
3.1.3 相册索引服务模块
相册索引服务模块,是基于 s2 算法的相册服务的核心模块。模块功能如下:
- 直接与数据模块交互,向使用者屏蔽数据层的数据操作细节,提供满足查询、搜索等需求的 api
- 查询指定 cell id 下的照片资源
- 查询指定 level 下,相册照片索引后的 cell id
- 查询以指定坐标点为圆心、指定半径范围内的照片
- 与用户相册扫描模块交互,获取新增照片和已经删除照片的数据,更新数据库内容,同时支持查询和通知更新状态
3.1.4 相册地点分类计算模块
相册地点分类计算模块是计算用户相册的地点分类结果的核心模块。该模块的主体功能如下:
- 获取 s2 相册索引服务中的照片 cell id,作为参数上传至服务端,服务端根据地图服务提供的聚合接口,将 cell id 的聚合结果返回给服务端
- 综合考量精确度和 cell id 的数据量,选取 level12 的 cell id 作为请求服务端的 cell id 等级
- 调用相册索引服务模块根据指定 level 获取 cell id 的方法得到去重后的 cell id
- 服务端返回的数据结构是 mdd_id(目的地 id) 与 cell id 的一对多的映射关系
- 利用本地 s2 相册索引服务中的照片 cell id,根据上一步服务端返回的分类数据进行分类
- 缓存每次地点分类的计算结果
3.2 整体流程
相册索引服务模块会在 app 启动时更新服务,将本地数据与相册数据同步。当用户触发地点相册功能时,相册地点分类计算模块会先取出缓存在本地相册地点分类计算结果展现给用户,同时驱动相册索引服务更新。
在收到更新服务更新完毕的通知后,首先向相册请求 12level 的全量去重的 cell id,然后将 cell id 上传服务端由服务端计算分类,最后结合相册索引服务的全量照片数据,计算照片的地点分类结果,缓存结果并渲染展现给用户。
3.3 性能优化
3.3.1 获取相册增量照片
相册索引服务模块需要同步服务和用户相册的照片资源数据,找到新增数据,加入到服务数据库中。最初设计的获取新增数据方案如下:
step.1 获取全量的用户相册的数据
step.2 遍历用户照片,查询是否存在本地服务数据库中
但是这个方案应用到照片量较大的手机上时,获取新增照片的时延很高。排查后我们发现原因在于全量遍历用户相册时延很高,同时在遍历中频繁查询数据库也比较耗时。经过调研发现,ios 的用户相册有「最近项目」的相册分类,该相册分类下的资源只按照添加顺序的倒序排列,即越新的照片越靠前。故将方案优化如下:
step.1:从列表头部截取 100 条
step.2:将该 100 条追加为新增照片
step.3:判断该 100 条中的最后一条,即新增时间最晚的一条,查询是否存在于服务数据库中
- 若不存在,继续 step.1
- 若存在,停止截取,从而得到新增照片
3.3.2 渐进计算相册照片的地点分类
相册地点分类计算模块在获得服务端返回的分类结果(mdd_id 与 cell id 列表的映射关系)后,根据结果对本地服务数据库中的照片进行分类。最初的方案如下:
step.1:遍历结果列表,获得每个 mdd_id 映射的 cell id 列表
- a. 遍历 cell id 列表,通过 cell id 向相册索引服务模块查询属于该 cell id 索引下的照片资源,从而获得该 mdd_id 对应的照片资源
- b. 对该目的地下的照片按照创建时间倒序排序
step.2:将所有目的地维度照片分类结果,按照每个结果集中照片最晚创建时间,即第一个照片的创建时间,进行倒序排序,获得按照地点维度和创建时间维度排序的地点相册的最终计算结果。
这样的方案导致在地点相册首次计算的时候,用户需要等待所有目的地下的结果计算完毕后才能展现给用户,同时需要多次按照创建时间排序,导致时延很高,冷启动下用户体验很差。
为此,我们做出了方案优化,减少排序次数,同时通过渐进加载的方式优化用户体验。主要思路是相册索引服务模块的数据库中,存储照片的创建时间可以通过 sql 查询,按照创建时间倒序排列的所有照片资源,获取倒序排列的照片资源集合:
step.1:每次从照片资源集合头部取 1000 条照片
- 遍历每一张照片,根据照片的 cell id,从 mdd_id-cell id 映射表中查询所属的目的地, 判断照片目的地分类结果集中是否存在该目的地的照片资源分类集合
- 存在,追加该照片
- 创建该目的地的结果集,追加到照片目的地分类结果集中,并追加该照片
step.2:将该 1000 张照片的分类结果渲染展现给用户
step.3:计算完所有照片的分类,通知结束渲染,计算完毕。
以上方案,将全量的本地照片资源以 1000 张为一批次,进行渐进计算,同时渐进渲染,缩短了用户的等待时间;同时,依托关系型数据库的排序能力,减少排序次数,优化了性能。
part.4 未来规划和总结
目前,本文介绍的基于 google-s2 算法实现的地点相册在马蜂窝 app ios 客户端已经上线一段时间,并且为笔记发布量带来了正向增长。但是这套方案在数据库数据处理中已经对于 google-s2 算法的使用上仍然有很大的优化和探索空间,后续我们团队也会对其不断优化和深挖。
google-s2 算法服务在马蜂窝 app ios 客户端中的实现和落地,成果不仅仅是满足了笔记发布场景的探索,更使得客户端具备了对于用户相册照片百米级精确度的索引和搜索的能力,可以为后续更多、更复杂的业务场景服务,相信在不远的未来能为用户提供更便捷、更有趣的旅行记录产品。
本文作者:王岩、王明友,马蜂窝内容业务研发工程师。