eCommerce Design by MongoDB

电商参考架构

[搭建灵活-可搜索-响应快速的产品目录系统][1]

大数据量电商如何使用MongoDB作为一个庞大产品目录持久层的一些最佳实践
多功能商品目录系统的建模和索引的最佳实践,包括商品及商品系列的查询、店铺价格以及支持多样化搜索的目录浏览

  • 对商品的搜索
  • 对商品系列的搜索
  • 对商品在每个店铺价格的检索
  • 允许目录的多方面搜索和浏览

商品数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//definition: db.product, 只展示了对每件商品而言最重要的信息,例如类别、品牌以及描述:
{
“_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”,

}
//find by ID
db.product.findOne({_id:"30671"});
//find by serias IDs
db.product.findOne({_id:{$in:["30617","45212"]}}); //$in operator
//find by
db.product.findOne({category:/^Shoes/Women/}); // regular express

系列数据模型

对产品目录而言另一个重要的考量是商品系列,例如现有尺寸、颜色以及风格。
对现有的、也许需要检索的商品系列(例如大小和颜色)而言的处理方法:

  • 在一个单一文档中存储一个商品以及它所有的系列
    这种方法拥有能够在一个单一查询中检索一个商品以及其所有系列的优点。
    然而,它并不是在所有情况下都是最好的方法。避免无限制的文档增长是一个非常重要的最佳实践。
    如果产品系列的数据以及它们相关数据非常小,在商品文档中存储这些数据也许会有意义。
  • 创建一个能够关联到主商品的、单独的系列数据模型 (pefer)

维护了在目录中展示主商品以及当用户请求一个更详细的产品视图时对每个系列的快速查询。
也可以保证商品以及系列文档的一个可预测大小

1
2
3
4
5
6
7
8
9
10
11
12
//definition: db.variantion  //关联到主商品的、单独的系列数据模型
{
“_id”: ”93284847362823”, //variant sku, 商品编号
“itemId”: “30671”, //references the main item
“size”: 6.0,
“color”: “red”

}
//通过它们的商品编号来快速检索到特定的商品系列 ???
db.variation.find({_id:”93284847362823”})
//通过对itemId 属性的查询获得某个特定商品的所有系列
db.variation.find({itemId:”30671”}).sort({_id:1})

不同店铺不同价格

Design: 将每个Price文档的_id设置为商品ID或者商品编号(SKU)的一个级联,
并且将商店ID与价格变量相关联,
这种方法也为处理价格提供很大的灵活性,因为它允许我们在商品或者系列级别对商品进行定价

  • 商品:30671_store23
  • 某个特定规格的商品:93284847362823_store23
    可以查询所有价格或者只是某个特定店铺的价格:
  • 所有价格:db.prices.find({_id:/^30671/})
  • 某个特定店铺的价格:db.prices.find({_id:/^30671_store23/})

    可以添加其他组合,
    例如每个店铺群的价格,然后在单个查询中使用$in操作符获取对于一个商品而言所有可能的价格:

1
2
3
4
5
6
7
8
db.prices.find({
_id:{$in:[
30671_store23”,
30671_sgroup12”,
93284847362823_store23”,
93284847362823_sgroup12”
]
})

浏览和搜索商品

挑战:

* 响应时间:在用户浏览的同时,结果的每个页面应该在毫秒内返回  
* 多个属性:伴随着用户选择不同的方面(例如,品牌、大小、颜色等),新的查询必须能够在多个文档属性中运行  
* 系列级别属性:一些用户选择的属性将会在商品级别进行查询,
  例如品牌,但是其它的查询则有可能运行于系列级别上,例如尺寸。
* 多个系列:每个商品都有可能有成千上万个系列,但是我们只希望每个商品只展示一次,因此,结果必须消除重复项。
* 排序:用户需要能够在多个属性上进行排序,例如价格、尺寸,此外排序操作必须能够高效运行。
* 分页:每个页面只返回少量结果,这就要求确定性排序。

Solution: MongoDB提供一个开源的[连接件项目][3]
它允许MongoDB和Apache Solr以及Elasticsearch同时使用。
然而,对于我们的参考架构,我们想完全在MongoDB中实现一个多方面搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 为了实现这个功能,我们创建了另一个集合,用于存储所谓的摘要文档。  
这些文档包含了我们需要基于多个搜索方面对产品目录中商品进行快速检索的所有信息。
{
“_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
]
}

通过使用这个数据模型,我们可以创建以下复合索引:
假设用户将会选择部门来重新定义他们的搜索结果。
对于没有部门的一个产品目录,我们可以非常轻易地从另一个像类别或者类型等比较普遍的方面开始。
然后,我们可以执行需要进行多方面搜索的查询,并且快速将结果返回到页面:

  • 部门+属性+类别+ _id
  • 部门+变量属性+类别+ _id
  • 部门+类别+ _id
  • 部门+价格+ _id
  • 部门+评分+ _id
1
2
3
4
5
6
7
8
//从商品ID获取摘要
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”})

[库存优化方法][2]

可以通过电商的店铺及应用访问到的、可靠的、集中的库存系统是提高和丰富用户体验中一个非常庞大的基础部分。

想要得到的一些特性:

  • 可靠地检查产品的实时库存
  • 提供用户在某个指定实体店提货的选项
  • 在某个商品有促销的情况下,判断每日补给的需求

设计原则

电商参考架构中的库存系统应该要做的事情:
需要的是构建一个高性能、可水平扩展的系统,
在一个庞大的、地理分布的区域中的店铺和用户都能够与MongoDB进行实时交互来查看和更新目录。

  • 提供一个库存的360°视图,可以在任何时间被任何客户端访问
  • 能够被任何需要库存数据的系统使用
  • 解决大数据量、以读取为主的工作负载,例如:库存检查
  • 解决大数据量的实时写操作,例如:库存更新
  • 支持批量写入操作以更新系统记录
  • 地理上分离
  • 伴随着库存中店铺数量或者商品数量的增多,保持水平扩展

店铺模式

用户案例的一个基本需求是为每个店铺维护一个关于所有库存的、集中的、实时的视图。
首先需要为店铺集合创建视图,从而将的库存与地理位置相联系起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 每个店铺都使用一个相当直接的文档
{
“_id”:ObjectId(“78s89453d8chw28h428f2423”),
“className”:”catalog.Store”,
“storeId”:”store100”,
“name”:”Bessemer Store”,
“address”:{
“addr1”:”1 Main St.”,
“city”:”Bessemer”,
“state”:”AL”,
“zip”:”12345”,
“country”:”USA”
},
“location”:[-86.95444, 33.40178],

}

创建下列的索引来优化在店铺数据中最经常使用读取类型:

  • {“storeId”:1},{“unique”:true}: 获取某个特定商店的库存
  • {“name”:1}:根据名字获取商店名称
  • {“address.zip”:1}: 获取一个邮编内的所有店铺,例如:店铺定位程序
  • {“location”: 2dsphere}:获取某一个特定地理位置周围的所有商店

库存数据模型

创建一个库存集合来跟踪每一个商品以及它们所有商品系列的真实库存量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//将数据从店铺集合复制到库存集合,最小化对数据库的来回读取数目,同时降低应用级的连接
// db.inventory
{
“_id”:”902372093572409542jbf42r2f2432”,
“storeId”:”store100”, // 知道哪个商店有什么商品是非常必要的
“location”:[-86.95444, 33.40178], // 查询离用户附近的库存
“productId”:”20034”, //
“vars”:[
{“sku”:”sku1”, “quantity”:”5”}, // 在商品级别文档中表示库存
{“sku”:”sku2”, “quantity”:”23”}, // 更大的文档以降低数据冗余度,可以减少在库存集合中需要查询或者更新的文档总数
{“sku”:”sku3”, “quantity”:”2”},

]
}

创建索引

  • {storeId:1}: 得到某一个指定商店库存中的所有商品
  • {productId:1},{storeId:1}: 获取一个指定店铺中某个产品的库存
  • {productId:1},{location:”2dsphere”}:获取在一定距离之内的某个产品的所有库存
1
2
3
4
5
6
7
8
// 可以基于’productID’查询我们的库存了
db.inventory.find({
“storeId”:”store100”,
“productId”:“20034”,
“vars.sku”:”sku11736”
},
{“vars.$”:1}
)

Reference

1
2
3