Learn

Real-time Local Inventory Search Using Redis

Will Johnston
Author
Will Johnston, Developer Growth Manager at Redis
Prasan Kumar
Author
Prasan Kumar, Technical Solutions Developer at Redis
GUTHUB CODE

Below is a command to the clone the source code for the application used in this tutorial

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

Real-time local inventory search is a method of utilizing advanced product search capabilities across a group of stores or warehouses in a region or geographic area by which a retailer can enhance the customer experience with a localized view of inventory while fulfilling orders from the closest store possible.

Geospatial search of merchandise local to the consumer helps sell stock faster, lowers inventory levels, and thus increases inventory turnover ratio. Consumers locate a product online, place the order in their browser or mobile device, and pick up at nearest store location. This is called “buy-online-pickup-in-store” (BOPIS)

Current challenges in real time inventory#

  • Over and under-stocking: While adopting a multi-channel business model (online & in store), lack of inventory visibility results in over and under-stocking of inventory in different regions and stores.
  • Consumers seek convenience: The ability to search across regional store locations and pickup merchandise immediately rather than wait for shipping is a key differentiator for retailers.
  • Consumers seek speed: All retailers, even small or family-run, must compete against the customer experience of large online retailers like Alibaba, FlipKart, Shopee, and Amazon.
  • High inventory costs: Retailers seek to lower inventory costs by eliminating missed sales from out-of-stock scenarios which also leads to higher “inventory turnover ratios.”
  • Brand value: Inaccurate store inventory counts lead to frustrated customers and lower sales. The operational pain will impact the status quo.
  • Accurate location/regional inventory search: Redis Cloud geospatial search capabilities enable retailers to provide local inventories by store location across geographies and regions based on a consumer's location. This enables a real-time view of store inventory and and seamless BOPIS shopping experience.
  • Consistent and accurate inventory view across multichannel and omnichannel experiences: Accurate inventory information no matter what channel the shopper is using, in-store, kiosk, online, or mobile. Redis Cloud provides a single source of truth for inventory information across all channels.
  • Real-time search performance at scale: Redis Cloud real-time search and query engine allows retailers to provide instant application and inventory search responses and scale performance effortlessly during peak periods.

Real-time local inventory search with Redis#

Redis provides geospatial search capabilities across a group of stores or warehouses in a region or geographic area allowing a retailer to quickly show the available inventory local to the customer.

Redis Cloud processes event streams, keeping store inventories up-to-date in real-time. This enhances the customer experience with localized, accurate search of inventory while fulfilling orders from the nearest and fewest stores possible.

This solution lowers days sales of inventory (DSI), selling inventory faster and carrying less inventory for increased revenue generation and profits over a shorter time period.

It also reduces fulfillment costs to home and local stores enhancing a retailer's ability to fulfill orders with the lowest delivery and shipping costs.

CUSTOMER PROOF POINTS

Building a real time local inventory search with redis#

GITHUB CODE

Below is a command to the clone the source code for the application used in this tutorial

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

Setting up the data#

Once the application source code is downloaded, run following commands to populate data in Redis:

# install packages
npm install

# Seed data to Redis
npm run seed

The demo uses two collections:

  • Product collection: Stores product details like productIdnamepriceimage, and other details
TIP

Download RedisInsight to view your Redis data or to play with raw Redis commands in the workbench.

  • StoresInventory collection: Stores product quantity available at different local stores.

For demo purpose, we are using the below regions in New York, US as store locations. Products are mapped to these location stores with a storeId and quantity.

Let's build the following APIs to demonstrate geospatial search using Redis:

  • InventorySearch API: Search Products in local stores within a search radius.
  • InventorySearchWithDistance API: Search Product in local stores within search radius and sort results by distance from current user location to store.

InventorySearch API#

The code that follows shows an example API request and response for the inventorySearch API:

inventorySearch API Request

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

inventorySearch API Response

{
  "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
}

When you make a request, it goes through the API gateway to the inventory service. Ultimately, it ends up calling an inventorySearch function which looks as follows:

/**
 * 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#

The code that follows shows an example API request and response for inventorySearchWithDistance API:

inventorySearchWithDistance API Request

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

inventorySearchWithDistance API Response

inventorySearchWithDistance API Response
{
  "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
}

When you make a request, it goes through the API gateway to the inventory service. Ultimately, it ends up calling an inventorySearchWithDistance function which looks as follows:

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;
}

Hopefully this tutorial has helped you visualize how to use Redis for real-time local inventory search across different regional stores. For additional resources related to this topic, check out the links below:

Additional resources#

Real time inventory with Redis

General