零售参考体系结构第1部分:构建灵活、可搜索、低延迟的产品目录


[这篇文章是布莱恩·雷内罗写的。]

产品目录数据管理是当今零售商面临的一个复杂问题。在多年依赖供应商提供的多个单一系统后,零售商现在正在重新考虑他们的选择,并展望未来。

在当今供应商提供的系统中,必须经常使用ETL过程来回移动产品数据,以确保所有系统都在同一个数据集上运行。就开发和管理而言,这种方法速度慢、容易出错且成本高。作为回应,零售商现在将数据服务作为集中的面向服务架构的一部分单独提供。

这是我们在MongoDB中常见的模式,以至于我们已经开始定义一些专门针对零售领域的最佳实践和参考体系结构。作为这项工作的一部分,今天我们将了解如何使用MongoDB实现目录服务,这是零售体系结构三部分系列的第一部分。

为什么是蒙古数据库?

许多不同的数据库类型都能够满足我们的产品目录用例,那么为什么选择MongoDB呢?

  • 文档灵活性:每个蒙古数据库文档可以存储表示为丰富的JSON结构的数据。这使得MongoDB非常适合存储任何东西,包括每个项目有数千个变体的非常大的目录。
  • 动态模式:每个文档中的JSON结构可以在任何时候改变,当需求改变时,允许增加灵活性和容易的数据重组。在MongoDB中,这些多个模式可以存储在一个集合中,并且可以使用共享索引,从而可以同时高效地搜索旧格式和新格式。
  • 表达式查询语言:跨许多文档属性执行查询的能力简化了许多任务。这还可以通过减少所需的数据库请求数量来提高应用程序性能。
  • 索引:MongoDB中提供了强大的二级、复合和地理索引选项,可以快速启用排序和基于位置的查询等功能。
  • 数据一致性:默认情况下,所有读取和写入都发送到蒙古数据库副本集的主要成员。这确保了很强的一致性,这对于零售商来说是一个重要的特征,因为他们可能有许多客户针对同一商品库存提出请求。
  • 地理分布的副本:由于数据源和客户端之间的地理距离而导致的网络延迟可能是有问题的,尤其是对于目录服务而言,该服务预计将支持大量低延迟读取。蒙古数据库副本集可以是地理分布的,因此它们靠近用户以便快速访问,在许多情况下减少了对副本集的需求。

这些只是蒙古数据库的几个特点,使它成为零售商的一个很好的选择。接下来,我们将了解如何在零售参考体系结构中使用这些特性来支持一些特性,包括:

  • 搜索项目和项目变体
  • 检索每个商店的商品价格
  • 通过多面搜索启用目录浏览

项目数据模型

我们首先需要考虑的是我们项目的数据模型。在下面的例子中,我们只显示了每个项目最重要的信息,如类别、品牌和描述:

{   “_id”: “30671”, //main item ID   “department”: “Shoes”,   “category”: “Shoes/Women/Pumps”,   “brand”: “Calvin Klein”,   “thumbnail”: “http://cdn.../pump.jpg”,   “title”: “Evening Platform Pumps”,   “description”: “Perfect for a casual night out or a formal event.”,   “style”: “Designer”,   …}

这种简单的数据模型允许我们根据最需要的标准轻松地查询项目。例如,使用db.collection.findOne,它将返回满足查询的单个文档:

  • 按标识获取项目
    db.definition.findOne({_id:”301671”})
  • 获取一组产品标识的项目
    db.definition.findOne({_id:{$in:[”301671”,”452318”]}})
  • 按类别前缀获取项目
    db.definition.findOne({category:/^Shoes\/Women/})

请注意第二个和第三个查询是如何使用$in运算符和正则表达式。当在适当索引的文档上执行时,MongoDB能够为这些类型的查询提供高吞吐量和低延迟。

可变数据模型

我们产品目录的另一个重要考虑因素是产品的变化,例如可用的尺寸、颜色和样式。我们上面的项目数据模型只捕获了关于每个目录项目的少量数据。那么,我们可能需要检索的所有可用项目变体(如大小和颜色)又是如何呢?

一种选择是将一个项目及其所有变体一起存储在一个文档中。这种方法的优点是能够在一个查询中检索一个项目和所有变体。然而,这并不是所有情况下的最佳方法。避免文档无限制增长是一个重要的最佳实践。如果变量及其相关数据的数量很少,将它们存储在项目文档中可能是有意义的。

另一个选项是创建一个单独的变量数据模型,该模型可以相对于主项目进行引用:

{
“_id”: ”93284847362823”, //variant sku
“itemId”: “30671”, //references the main item
“size”: 6.0,
“color”: “red”
…
}

该数据模型允许我们通过SKU编号快速查找特定的项目变量:

db.variation.find({_id:”93284847362823”})

以及特定项目的所有变体itemId属性:

db.variation.find({itemId:”30671”}).sort({_id:1})

通过这种方式,我们可以快速查询目录中显示的主要项目,以及用户请求更具体产品视图时的每个变体。我们还确保项目和变体文档的可预测大小。

每店定价

为我们的产品目录定义参考体系结构时,另一个考虑因素是定价。我们现在已经看到了一些方法,可以将我们项目的数据模型结构化,以便直接或基于特定属性快速检索项目。价格会因很多因素而变化,比如商店的位置。我们需要一种方法来快速检索任何给定项目或项目变体的具体价格。对于大型零售商来说,这可能是一个很大的问题,因为一个有一百万个商品和一千个商店的目录意味着我们必须查询十亿个文件的集合才能找到任何给定商品的价格。

当然,我们可以将每个变体的价格存储为项目文档中的嵌套文档,但是更好的解决方案是再次利用MongoDB的查询速度_id。例如,如果我们目录中的每个项目都由一个项目标识引用,而每个变体都由一个SKU编号引用,那么我们可以设置_id每个文档都是与该价格变量相关联的项目标识或SKU和商店标识的串联。使用此模型,将_id对于双泵和它上面描述的红色变体,看起来像这样:

  • 项目:30671_store23
  • 变体:93284847362823_store23

这种方法也为处理定价提供了很大的灵活性,因为它允许我们在项目或变体级别对项目进行定价。然后,我们可以查询所有价格或仅查询特定位置的价格:

  • 所有价格:db.prices.find({_id:/^30671/})
  • 商店价格:db.prices.find({_id:/^30671_store23/})

我们甚至可以添加其他组合,例如每个商店组的定价,并通过使用$in操作员:

db.prices.find({_id:{$in:[“30671_store23”,
“30671_sgroup12”,
“93284847362823_store23”,
“93284847362823_sgroup12” ]}})

浏览和搜索产品

我们的产品目录面临的最大挑战是支持多面搜索浏览。虽然许多用户希望在我们的产品目录中搜索他们正在寻找的特定项目或标准,但也有许多用户希望浏览,然后通过任意数量的属性缩小返回的结果。考虑到创建这样一个页面的需要:

Sample catalog

我们面临许多挑战:

  • 响应时间:当用户浏览时,结果的每一页都应该以毫秒为单位返回。
  • 多种属性:当用户选择不同的方面(例如品牌、尺寸、颜色)时,新的查询必须在多个文档属性上运行。
  • 变体级别属性:一些用户选择的属性将在项目级别进行查询,如品牌,而其他属性将在变体级别进行查询,如大小。
  • 多个变量:每个项目可以有数千个变量,但是我们只想显示每个项目一次,所以结果必须重复。
  • 排序:需要允许用户对多个属性进行排序,如价格和大小,并且排序操作必须高效执行。
  • 分页:每页应该只返回少量的结果,这需要确定性的排序。

许多零售商可能希望使用专用搜索引擎作为这些功能的基础。MongoDB提供了一个开放源代码connector project,它允许在MongoDB中使用Apache Solr和弹性搜索。然而,对于我们的参考体系结构,我们希望完全在MongoDB中实现分面搜索。

为了实现这一点,我们创建了另一个集合来存储我们称之为摘要文档的内容。这些文档包含了我们需要的所有信息,以便根据不同的搜索面在目录中快速查找项目。

{ 
“_id”: “30671”,
“title”: “Evening Platform Pumps”,
“department”: “Shoes”,
“Category”: “Women/Shoes/Pumps”,
   “price”: 149.95,
“attrs”: [“brand”: “Calvin Klein”, …],
“sattrs”: [“style”: ”Designer”, …],
“vars”: [
{
“sku”: “93284847362823”,
“attrs”: [{“size”: 6.0}, {“color”: “red”}, …],
“sattrs”: [{“width”: 8.0}, {“heelHeight”: 5.0}, …],
}, … //Many more SKUs
]

}

请注意,在这个数据模型中,我们定义了属性和次要属性。虽然用户可能希望能够搜索项目或项目变体的许多不同属性,但是只有一个最常用的核心集。例如,给定一双鞋,用户根据可用尺寸过滤他们的搜索可能比根据鞋跟高度过滤更常见。通过同时使用attrsattr在我们的数据模型中,我们能够使所有这些项目属性都可供搜索,但是仅通过索引来产生索引最常用属性的开销attr

使用这个数据模型,我们将根据以下组合创建复合指数:

  • 部门+ attr +类别+ _id
  • 部门+ vars.attr +类别+id
  • 部门+类别+ _id
  • 部门+价格+ _id
  • 部门+评级+ _id

在这些索引中,我们总是从部门开始,我们假设用户会选择部门来优化他们的搜索结果。对于没有部门的目录,我们可以很容易地从另一个共同的方面开始,比如类别或类型。然后,我们可以执行多面搜索所需的查询,并将结果快速返回到页面:

  • 从itemId获取摘要
    db.variation.find({_id:”30671”})
  • 获取特定项目变体的摘要
    db.variation.find({vars.sku:”93284847362823”},{“vars.$”:1})
  • 按部门获取所有项目的摘要
    db.variation.find({department:”Shoes”})
  • 获取混合参数的摘要
    db.variation.find({ “department”:”Shoes”,
     “vars.attr”: {“color”:”red”},
     “category”: “^/Shoes/Women”})

翻新的轮胎

我们研究了一些产品目录的建模和索引数据的最佳实践,该产品目录支持多种应用功能,包括项目和项目变体查找、商店定价和使用多面搜索的目录浏览。使用这些方法作为起点可以帮助您为自己的实现找到最佳设计。

了解更多信息

要了解如何使用MongoDB重新想象零售体验,read our white paper。在本文中,您将了解新的零售挑战以及MongoDB如何应对这些挑战。

要了解MongoDB的咨询团队如何更快地将您的应用投入使用,请访问我们的快速启动项目。