学习

使用 Redis 进行实时本地库存搜索

Will Johnston
作者
Will Johnston, Redis 的开发者成长经理
Prasan Kumar
作者
Prasan Kumar, Redis 的技术解决方案开发者
GUTHUB 代码

以下是用于克隆本教程中使用的应用程序源代码的命令

git clone https://github.com/redis-developer/redis-real-time-inventory-solutions

实时本地库存搜索 是一种利用先进的产品搜索功能在某个地区或地理区域的一组商店或仓库中搜索的方法,零售商可以通过此方法增强客户体验,提供本地化的库存视图,同时从最接近的商店完成订单。

与消费者所在地相关的商品的地理空间搜索有助于更快地销售库存,降低库存水平,从而提高库存周转率。消费者在线找到产品,在浏览器或移动设备中下单,然后在最近的商店取货。这被称为“在线购买,店内取货”(BOPIS)

实时库存中的当前挑战#

  • 过度和不足库存:在采用多渠道业务模式(线上和线下)的同时,缺乏库存可见性会导致不同地区和商店的库存过度和不足。
  • 消费者 寻求便利:能够在区域商店位置搜索并 立即 提取商品,而不是等待运输,是零售商的关键差异化因素。
  • 消费者 寻求速度:所有零售商,即使是小型或家族经营的零售商,也必须与阿里巴巴、FlipKart、Shopee 和亚马逊等大型在线零售商的 客户体验 竞争。
  • 高库存成本:零售商试图通过消除缺货情况下的漏单来降低库存成本,这也导致更高的“库存周转率”。
  • 品牌价值:商店库存计数不准确会导致客户感到沮丧,销售额下降。运营上的痛苦将 影响现状
  • 准确的位置/区域库存搜索:Redis Cloud 地理空间搜索功能使零售商能够根据消费者的位置,在跨地域和区域的商店位置提供本地库存。这使零售商能够实时查看商店库存,并提供无缝的 BOPIS 购物体验。
  • 在多渠道和全渠道体验中保持一致且准确的库存视图:无论购物者使用哪个渠道,无论是在店里、售货亭、在线还是手机上,都能获得准确的库存信息。Redis Cloud 为所有渠道提供库存信息的单一来源。
  • 大规模实时搜索性能:Redis Cloud 实时搜索和查询引擎使零售商能够在高峰期提供即时应用程序和库存搜索响应,并毫不费力地扩展性能。

使用 Redis 进行实时本地库存搜索#

Redis 提供地理空间搜索功能,可以跨某个地区或地理区域的一组商店或仓库进行搜索,使零售商能够快速显示本地可用的库存。

Redis Cloud 处理事件流,实时更新商店库存。这通过提供本地化的准确库存搜索来增强客户体验,同时尽可能从最接近且数量最少的商店完成订单。

此解决方案降低了库存周转天数 (DSI),更快地销售库存,并减少库存以在更短的时间内提高收入和利润。

它还降低了到家和本地商店的履行成本,增强了零售商以最低的送货和运输成本履行订单的能力。

客户证明要点

使用 Redis 构建实时本地库存搜索#

GITHUB 代码

以下是用于克隆本教程中使用的应用程序源代码的命令

git clone https://github.com/redis-developer/redis-real-time-inventory-solutions

设置数据#

下载应用程序源代码后,运行以下命令将数据填充到 Redis 中

# install packages
npm install

# Seed data to Redis
npm run seed

演示使用两个集合

  • 产品集合:存储产品详细信息,例如 productIdnamepriceimage,以及其他详细信息
提示

下载 RedisInsight 以查看您的 Redis 数据或在工作台中使用原始 Redis 命令进行操作。

  • 商店库存集合:存储不同本地商店可用的产品数量。

为了演示目的,我们使用美国纽约的以下地区作为商店位置。产品与这些位置的商店关联,并带有 storeId 和 quantity。

让我们构建以下 API 来演示使用 Redis 进行地理空间搜索

  • 库存搜索 API:在搜索半径内搜索本地商店中的产品。
  • 带距离的库存搜索 API:在搜索半径内搜索本地商店中的产品,并按当前用户位置到商店的距离对结果进行排序。

库存搜索 API#

以下代码显示了 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;
}

带距离的库存搜索 API#

以下代码显示了 inventorySearchWithDistance API 的示例 API 请求和响应

带距离的库存搜索 API 请求

POST https://localhost:3000/api/inventorySearchWithDistance
{
  "sku": 1019688,
  "searchRadiusInKm": 500,
  "userLocation": {
    "latitude": 42.88023,
    "longitude": -78.878738
  }
}

带距离的库存搜索 API 响应

带距离的库存搜索 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 函数,该函数如下所示

src/inventory-service.ts
/**
 * 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 进行实时本地库存搜索。有关此主题的更多资源,请查看以下链接。

其他资源#