以下是用于克隆本教程中使用的应用程序源代码的命令
git clone https://github.com/redis-developer/redis-real-time-inventory-solutions
实时本地库存搜索 是一种利用先进的产品搜索功能在某个地区或地理区域的一组商店或仓库中搜索的方法,零售商可以通过此方法增强客户体验,提供本地化的库存视图,同时从最接近的商店完成订单。
与消费者所在地相关的商品的地理空间搜索有助于更快地销售库存,降低库存水平,从而提高库存周转率。消费者在线找到产品,在浏览器或移动设备中下单,然后在最近的商店取货。这被称为“在线购买,店内取货”(BOPIS)
Redis 提供地理空间搜索功能,可以跨某个地区或地理区域的一组商店或仓库进行搜索,使零售商能够快速显示本地可用的库存。
Redis Cloud 处理事件流,实时更新商店库存。这通过提供本地化的准确库存搜索来增强客户体验,同时尽可能从最接近且数量最少的商店完成订单。
此解决方案降低了库存周转天数 (DSI),更快地销售库存,并减少库存以在更短的时间内提高收入和利润。
它还降低了到家和本地商店的履行成本,增强了零售商以最低的送货和运输成本履行订单的能力。
以下是用于克隆本教程中使用的应用程序源代码的命令
git clone https://github.com/redis-developer/redis-real-time-inventory-solutions
下载应用程序源代码后,运行以下命令将数据填充到 Redis 中
# install packages
npm install
# Seed data to Redis
npm run seed
演示使用两个集合
productId
, name
, price
, image
,以及其他详细信息下载 RedisInsight 以查看您的 Redis 数据或在工作台中使用原始 Redis 命令进行操作。
为了演示目的,我们使用美国纽约的以下地区作为商店位置。产品与这些位置的商店关联,并带有 storeId 和 quantity。
让我们构建以下 API 来演示使用 Redis 进行地理空间搜索
以下代码显示了 inventorySearch
API 的示例 API 请求和响应
库存搜索 API 请求
{
"sku":1019688,
"searchRadiusInKm":500,
"userLocation": {
"latitude": 42.880230,
"longitude": -78.878738
}
}
库存搜索 API 响应
{
"data": [
{
"storeId": "02_NY_ROCHESTER",
"storeLocation": {
"longitude": -77.608849,
"latitude": 43.156578
},
"sku": 1019688,
"quantity": 38
},
{
"storeId": "05_NY_WATERTOWN",
"storeLocation": {
"longitude": -75.910759,
"latitude": 43.974785
},
"sku": 1019688,
"quantity": 31
},
{
"storeId": "10_NY_POUGHKEEPSIE",
"storeLocation": {
"longitude": -73.923912,
"latitude": 41.70829
},
"sku": 1019688,
"quantity": 45
}
],
"error": null
}
当您发出请求时,它会通过 API 网关进入 库存服务
。最终,它会调用一个 inventorySearch
函数,该函数如下所示
/**
* Search Product in stores within search radius.
*
* :param _inventoryFilter: Product Id (sku), searchRadiusInKm and current userLocation
* :return: Inventory product list
*/
static async inventorySearch(_inventoryFilter: IInventoryBodyFilter): Promise<IStoresInventory[]> {
const nodeRedisClient = getNodeRedisClient();
const repository = StoresInventoryRepo.getRepository();
let retItems: IStoresInventory[] = [];
if (nodeRedisClient && repository && _inventoryFilter?.sku
&& _inventoryFilter?.userLocation?.latitude
&& _inventoryFilter?.userLocation?.longitude) {
const lat = _inventoryFilter.userLocation.latitude;
const long = _inventoryFilter.userLocation.longitude;
const radiusInKm = _inventoryFilter.searchRadiusInKm || 1000;
const queryBuilder = repository.search()
.where('sku')
.eq(_inventoryFilter.sku)
.and('quantity')
.gt(0)
.and('storeLocation')
.inRadius((circle) => {
return circle
.latitude(lat)
.longitude(long)
.radius(radiusInKm)
.kilometers
});
console.log(queryBuilder.query);
/* Sample queryBuilder query
( ( (@sku:[1019688 1019688]) (@quantity:[(0 +inf]) ) (@storeLocation:[-78.878738 42.88023 500 km]) )
*/
retItems = <IStoresInventory[]>await queryBuilder.return.all();
/* Sample command to run query directly on CLI
FT.SEARCH StoresInventory:index '( ( (@sku:[1019688 1019688]) (@quantity:[(0 +inf]) ) (@storeLocation:[-78.878738 42.88023 500 km]) )'
*/
if (!retItems.length) {
throw `Product not found with in ${radiusInKm}km range!`;
}
}
else {
throw `Input params failed !`;
}
return retItems;
}
以下代码显示了 inventorySearchWithDistance
API 的示例 API 请求和响应
带距离的库存搜索 API 请求
{
"sku": 1019688,
"searchRadiusInKm": 500,
"userLocation": {
"latitude": 42.88023,
"longitude": -78.878738
}
}
带距离的库存搜索 API 响应
{
"data": [
{
"storeId": "02_NY_ROCHESTER",
"storeLocation": {
"longitude": -77.608849,
"latitude": 43.156578
},
"sku": "1019688",
"quantity": "38",
"distInKm": "107.74513"
},
{
"storeId": "05_NY_WATERTOWN",
"storeLocation": {
"longitude": -75.910759,
"latitude": 43.974785
},
"sku": "1019688",
"quantity": "31",
"distInKm": "268.86249"
},
{
"storeId": "10_NY_POUGHKEEPSIE",
"storeLocation": {
"longitude": -73.923912,
"latitude": 41.70829
},
"sku": "1019688",
"quantity": "45",
"distInKm": "427.90787"
}
],
"error": null
}
当您发出请求时,它会通过 API 网关进入库存服务。最终,它会调用一个 inventorySearchWithDistance
函数,该函数如下所示
/**
* Search Product in stores within search radius, Also sort results by distance from current user location to store.
*
* :param _inventoryFilter: Product Id (sku), searchRadiusInKm and current userLocation
* :return: Inventory product list
*/
static async inventorySearchWithDistance(_inventoryFilter: IInventoryBodyFilter): Promise<IStoresInventory[]> {
const nodeRedisClient = getNodeRedisClient();
const repository = StoresInventoryRepo.getRepository();
let retItems: IStoresInventory[] = [];
if (nodeRedisClient && repository && _inventoryFilter?.sku
&& _inventoryFilter?.userLocation?.latitude
&& _inventoryFilter?.userLocation?.longitude) {
const lat = _inventoryFilter.userLocation.latitude;
const long = _inventoryFilter.userLocation.longitude;
const radiusInKm = _inventoryFilter.searchRadiusInKm || 1000;
const queryBuilder = repository.search()
.where('sku')
.eq(_inventoryFilter.sku)
.and('quantity')
.gt(0)
.and('storeLocation')
.inRadius((circle) => {
return circle
.latitude(lat)
.longitude(long)
.radius(radiusInKm)
.kilometers
});
console.log(queryBuilder.query);
/* Sample queryBuilder query
( ( (@sku:[1019688 1019688]) (@quantity:[(0 +inf]) ) (@storeLocation:[-78.878738 42.88023 500 km]) )
*/
const indexName = `${StoresInventoryRepo.STORES_INVENTORY_KEY_PREFIX}:index`;
const aggregator = await nodeRedisClient.ft.aggregate(
indexName,
queryBuilder.query,
{
LOAD: ["@storeId", "@storeLocation", "@sku", "@quantity"],
STEPS: [{
type: AggregateSteps.APPLY,
expression: `geodistance(@storeLocation, ${long}, ${lat})/1000`,
AS: 'distInKm'
}, {
type: AggregateSteps.SORTBY,
BY: "@distInKm"
}]
});
/* Sample command to run query directly on CLI
FT.AGGREGATE StoresInventory:index '( ( (@sku:[1019688 1019688]) (@quantity:[(0 +inf]) ) (@storeLocation:[-78.878738 42.88023 500 km]) )' LOAD 4 @storeId @storeLocation @sku @quantity APPLY "geodistance(@storeLocation,-78.878738,42.88043)/1000" AS distInKm SORTBY 1 @distInKm
*/
retItems = <IStoresInventory[]>aggregator.results;
if (!retItems.length) {
throw `Product not found with in ${radiusInKm}km range!`;
}
else {
retItems = retItems.map((item) => {
if (typeof item.storeLocation == "string") {
const location = item.storeLocation.split(",");
item.storeLocation = {
longitude: Number(location[0]),
latitude: Number(location[1]),
}
}
return item;
})
}
}
else {
throw `Input params failed !`;
}
return retItems;
}
希望本教程能帮助您直观地了解如何在不同的区域门店之间使用 Redis 进行实时本地库存搜索。有关此主题的更多资源,请查看以下链接。