大家好! 我叫Denis Girko,我是Lamoda电子商务平台的系统架构师。 去年,我在DevConf会议上发表了一份报告 ,希望与您分享。
这是一份有关大型在线商店在订单交付过程中遇到的困难以及哪些技术解决方案可以帮助克服这些困难的评论报告(以我们在Lamoda进行测试的解决方案为例)。

那会是什么? 我告诉你:
- 关于交付过程并发现问题;
- 如何有效地将交货地区存储在数据库中;
- 如何提高我们从客户那里收到的数据的质量;
- 如何在地址数据库中搜索收件人以找到更准确的结果。
拉莫达一般订单交付计划
Lamoda是一个在线商店,有四个送货国家:俄罗斯,乌克兰,哈萨克斯坦,白俄罗斯。 由于我们拥有自己的送货服务以及我们使用其服务的十二个第三方合作伙伴,因此我们在第二天就交付了货物。 交付是我们业务的很大一部分。
Lamoda接受订单,在注册时向客户询问地址,并将其传递给快递服务。

如果我们没有一项快递服务,而只有几项快递服务怎么办? 然后添加下一步-确定我们将接受订单的送货服务。

可能会有一些业务选择标准。 但是首先要考虑的是,该快递服务是否可以将服务交付到客户选择的城市。 因此,将任何快递公司整合到我们的系统中的第一步是确定其覆盖范围。

接下来,您需要学习如何检查客户的地址是否属于此区域。
总体方案将得到改进,如下所示:
- 询问地址;
- 找出可以提供哪些快递服务;
- 选择一个你想要的

现在,进一步了解这些步骤。
我们要求客户提供地址
我怎么问他
- 要求填写一个大字段。 客户敲打他的地址,这进一步不需要任何棘手的操作。 地址可以打印在一张纸上,然后递给快递员,然后快递员自己弄清楚。
- 第二种选择更为复杂。 我们要求客户在其字段中填写地址的每个部分。 在这里您已经可以做一些事情。 例如,将莫斯科市与给定的城市列表进行比较。 但是这样做会很不好,因为莫斯科市可以用多种方式书写: 莫斯科”,“莫斯科城市”,“莫斯科城市”无空格等。
- 因此,有一个更高级的选项。 由于我们拥有的城市列表是有限的,因此您可以预编译一个城市列表,并建议客户选择他需要的城市。 好处是,对于这样一个列表的每个元素,我们已经可以在此处关联一些标识符。 我们的开发人员喜欢不使用字符串,而是使用可以在我们所有系统中用作所选城市的标识符的标识符。 我在幻灯片上有一个中央邮局索引作为标识符。

我们要运送什么送货服务?
由于我们有一个标识符(索引),因此让存储在我们数据库中的区域由索引列表表示。 在这种情况下,检查城市进入该地区的算法非常简单。 因此,让我们做吧:我们将从快递服务接收的交货地区以索引的形式放置在数据库中。
索引各有利弊。 我先要说一下,Lamoda一开始就是这样做的:选择城市客户的结果是一个索引,而我们的索引存储在数据库中。 为什么加? 正如我所说,索引是每个人都可以理解的东西。 任何刚上班的经理都知道什么是索引。 他可以从城市的快递公司收到,以某种方式将它们转换为索引并使用。 缺点是该索引是俄罗斯邮政局的标识符。 附近的住区可以共享相同的索引。
为什么缺少索引?
一个简单的例子:Lyubertsy。 附近是Marusino村。 Marusino没有邮局;他们的信件来往Lyubertsy的邮局之一。 如果我们想增加交货到Lyubertsy,而不是增加到Marusino,因为这可能对我们没有经济上的利益,我们将无法仅通过索引来做到这一点。

另一个例子是拉莫达在莫斯科扩张并开设了第二个转运仓库。 有必要将莫斯科分为北部和南部两半。 在下订单时,已经知道要从哪个转运仓库进行交货。 在这种情况下,每个城市只有一个指数是不够的。
我们决定将地理坐标与索引一起使用。 我们获取客户的地址,然后通过Yandex地址解析器运行它。 在输出中,我们不仅获得索引,还获得坐标。 我们在细节不重要的情况下使用索引。 坐标指定了需要对区域进行细划的情况。
他们在后勤人员的设置程序中提供了一个界面,使您可以在地图顶部绘制一个多边形。 很简单:问题落入垃圾填埋场-有交付,没有下降-不。

多边形区域创建界面
我们为每个订单提供地理坐标的好处是可以改善后勤人员用来为销售代表制定路线的界面。 界面显示一个地图,上面标记了客户订单。 后勤人员使用套索工具,将相邻的订单合并为一条路线。 此外,这条路线通向一个销售代表,也就是说,一个人白天不需要从城市的一端到另一端去接他的所有订单-他们在地理位置上都很接近。

路由接口
客户输入的地址将转换为坐标。 我们获得给定地址的坐标的可能性直接取决于客户输入的地址的质量。 因此,我们首先想到的是如何增加公认的地址数量。 因此,您需要帮助客户输入正确的地址。
事实是,客户通常不遵循我们为他们提供的方案,因此我们获得了向其交付订单的4个国家/地区中每个国家的地址数据库。 他们不仅对城市,对街道,甚至对门牌号,都表现出了最大的热情。 为了列出房屋,我们解析了openstreetmap.org的开放数据。

结帐表单提供了一些技巧,可以使地址数据形式化
地址库
要在地址基础上进行请求,您需要将其保留在家里。 我们从哪里获得四个国家的所有地址基础? 在俄罗斯, FIAS是我们的税务服务部门汇编和维护的地址库。 它很完整,尽管并非没有缺陷。 我们的交付合作伙伴已帮助我们与其他国家/地区合作。

我们还提供了一组PHP脚本,这些脚本采用了地址库的格式,并且以相同的方式将其添加到PostgreSQL中 。 为什么以相同的形式? 因为任务之一是定期从相同来源更新这些数据库。 这意味着,如果我们提供了转换,则每次更新都必须重复进行转换,因此,数据进入PostgreSQL,然后从那里转换并存储在Apache Solr中 ; Solr使您可以快速搜索它们并做到明智。 一个小型的PHP Web服务器能够在Solr中构建请求,根据它们的结果,将为该站点上的客户端创建一个针对客户端的列表。

我们从数据源中下载数据的方式大致与收到数据时相同。 也就是说,具有相同的字段集,相同的列类型,依此类推。 按原样添加它们。 我们从一开始就尝试使用这种形式的数据,为了将其转换为可以使用的结构,我们编写了几种视图。 因为我们有4个国家,所以所有这4个国家都乘以4,因此支持起来非常困难且昂贵。 因此,有必要对此做一些事情。
我们摆脱的第一件事是在早期阶段是非结构化的,或者说是特定的结构化 。 也就是说,原始数据一经上传,就借助视图将其转换为统一格式,并通过其进一步配置所有其他转换。 这使我们免于乘以4。这时我们就忘记了数据传给我们的结构,而我们只能使用自己为自己创造的东西工作。

如果您需要两个资源-请下载。 最主要的是,转换为视图后,此数据输出的格式是相同的。

加载地址数据库的另一个要求是必须对其进行点更正。 一个简单的例子:在FIAS中,楚瓦什共和国称为“楚瓦什共和国。 “ Chuvashia。” 好吧,我们只想要楚瓦什共和国。 为什么我们需要这个破折号? 同时,我们仍然无法避免来自源的定期更新。
这是我们在PostgreSQL中拥有的以下几层。

左侧的表格是从源下载的原始数据。
它们的后面是将数据转换为标准格式的视图。
局部替代是我们的表集,这些表逐点重新定义了已加载地址数据的某些属性。 例如,我们在此处包括应接收具有此类标识符的记录,而不是“ Chuvash rep。”。 -Chuvashia”是我们选择的名称。
映射表是我们的标识符存储库,我们自己将其分配给下载的地址对象-这使我们能够从源中抽象出系统,从源中使用的那些标识符中提取系统,并且还可以在一个ID下隐藏一个源,甚至隐藏多个源我待会再讲。 所有这些结合在一起并固定在实体化视图中。 因此,我们几乎获得了最终表的等效表,可以通过运行一个SQL命令REFRESH MATERIALIZED VIEW
对其进行更新。
地址对象 -形成的地址基础,包括所有更正和添加。
因此,在输出中,我们已经使用新名称和标识符纠正了地址对象。 所有这些都经过转换和非规范化,以方便搜索,并添加到Solr中。
既然我们现在有了地址数据库,那么使用它们不仅可以使订购单更加有趣,而且可以进行搜索。 搜索在哪里可以派上用场? 事实证明,很多地方。 我们从快递服务接收到的相同交付区域通常仅由城市列表来表示。 而且,城市列表充满了与用户输入相同的问题:城市可以具有不同的解释,不同的名称等等。
我在这里有一张特别的幻灯片,有这样一个恐怖的故事-如果我们手动将所有内容都转换为PHP:那是车臣共和国,车臣共和国,那么对于每个数据源,我们该怎么办-地狱就是地狱。

另外:在屏幕上-服务中的一段真实代码,由于描述的解决方案而变得不必要。
我们对这些问题进行了分类。
1)相同对象的等效名称。 例如,诸如Chuvashia和Chuvash Republic之类的常见同义词。
2)重命名城市。 乌克兰现在正处于摆脱共产主义过去的活跃阶段,这就是为什么他们实际上每天都在改变定居点的名称的原因。 因此,可能会发现一个数据库中有旧名称,而另一个数据库中有新名称。
3)很多错误。 人们常常误以为定居点的地位。 有一个村庄,这里是一个村庄,或者这里是一个村庄,有一个农场。
4)将外来音译成俄文,通常以不同的方式音译同一个名字。
5)层次结构中存在许多错误:按习惯,泽列诺格勒属于莫斯科地区,尽管正式也将其列为莫斯科的FIAS。 正确地写“莫斯科市,泽列诺格勒。”
我们是怎么想到的?
我们要做的第一件事是将地址中所有无关紧要的部分与名称分开。 我们不会丢弃它们,它们会参与搜索,但会与重要部分分开。

接下来,我们列出了名称中使用的常见同义词和缩写的简短列表。 在源允许的地方,我们加载了所有名称并将其放在Solr中。 不仅最相关,而且还可能有同义词和历史名称。
为了更好地进行搜索,我们将可能引起差异的所有字母都排除在外。 这适用于俄语以及我们仍然需要处理的那些语言。
我们纠正了错别字并修复了布局。
最后,我们提出了幻像父母-这些是分配给对象的父母。 它们与搜索相关,但不参与搜索结果。 例如,对于Zelenograd,我们添加了莫斯科地区。 现在,您可以搜索“ Zelenograd莫斯科地区”并找到我们需要的对象,但是在搜索结果中,它仍然是正确的“ Zelenograd莫斯科地区”。

根据业务需求,需要不同的搜索精度等级。 因此,我们有4个度数,每个度数给出的结果概率较高,但是概率较低的正是所寻求的结果。

在哪里找到这样的搜索应用程序?
- 再次,我们通过这样的搜索来运行客户输入的地址。 如果他没有使用结帐页面上的提示,那么我们就有更多机会将他输入的行转换为标识符。 我们得到正式地址。
- 我们通过搜索进行快递服务发送给我们的所有信息-我们认识到他们传递给我们的城市。 这使我们每天只能发布10件商品,这与B2B有关-Lamoda向第三方公司提供送货服务,因此每单位时间有很多新的快递服务连接。
- 这使我们能够将各种有用的信息“串”入地址数据库的标识符中。 例如,我们下载了时区IP地址,以按客户的IP地址搜索城市。
- 现在,我们有机会隐藏使用我们的标识符之一从两个来源合并而成的地址库。 也就是说,它允许避免重复并在两个基址中匹配相同的地址对象。
我们不会停止。 这是我们仍然可以改进的过程。
首先,Lamoda致力于索引。 也就是说,我们的标识符是索引,我们知道它们的缺点。 几乎我们所有的系统都已切换到新的API;它们不使用索引运行,而是使用我们自己分配给地址对象的标识符来运行。 优点是检查领土内的城市就像使用索引一样简单。 但是,在一个ID后面可以隐藏多个定居点这一事实无可厚非。
另外:时间从我演讲的那一刻起已经过去了,现在我很乐意纠正自己:我们的索引现在仅在这种情况下才可用,即快递员以列表的形式向我们提供其领土时,例如俄罗斯邮政。 在其他情况下,索引被我们的内部地址标识符取代。
幻灯片上是界面的一部分,可让您手动配置区域,但实际上,所有内容都是通过以字符串形式批量加载的地址对象列表进行配置的。

我们从openstreetmap.org下载房屋的地理坐标。 现在,在大多数情况下,我们无需去外部服务即可确定位置。 这使我们减少了10次去Yandex的行程,这自然节省了钱。
我们在地址数据的搜索链中摆脱了PHP。 我们重写了在Lua上访问Solr的代码。 用Openresty代替了nginx,现在一切都非常快并且可以承受重负荷。 我们搜索服务中95%的响应都在10毫秒内完成,这对我们来说绰绰有余。

另外:使用Openresty和Lua吸引了他们的性能,这是一种回报丰厚的实验:服务快速运行,在负载下稳定并且易于维护。 但是从那时起,Lamoda就采用了与加载的后端的一种编程语言相同的质量Golang。 如果现在就决定开发服务,我们会更喜欢它。
结论
从所有工作中我得出的个人道德是,地址数据是您无法期望理想数据质量的领域。 这将永远不会发生。 我们将永远不会从客户或外部来源获得完美的数据。 因此,您必须从中挤出最大的收益。