学习

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

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

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

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 进行地理空间搜索

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

InventorySearch API#

以下代码展示了 inventorySearch API 的示例 API 请求和响应。

inventorySearch API 请求

{
    "sku":1019688,
    "searchRadiusInKm":500,
    "userLocation": {
        "latitude": 42.880230,
        "longitude": -78.878738
    }
}

inventorySearch 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 网关到达 inventory service。最终,它会调用一个 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#

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

inventorySearchWithDistance API 请求

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

inventorySearchWithDistance API 响应

inventorySearchWithDistance 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 网关到达 inventory service。最终,它会调用一个 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 进行实时本地库存搜索。有关此主题的更多资源,请查看下面的链接。

更多资源#

使用 Redis 的实时库存

通用