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)
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.
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
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:
productId
, name
, price
, image
, and other detailsDownload RedisInsight to view your Redis data or to play with raw Redis commands in the workbench.
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:
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;
}
The code that follows shows an example API request and response for inventorySearchWithDistance
API:
inventorySearchWithDistance API Request
{
"sku": 1019688,
"searchRadiusInKm": 500,
"userLocation": {
"latitude": 42.88023,
"longitude": -78.878738
}
}
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:
/**
* 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: