在本教程中,您将获得使用 Redis 的地理位置搜索功能的实际知识和实践经验,尤其侧重于其在电子商务平台的微服务架构中的应用。以下内容是您期望学到的内容
以下是克隆本教程中使用的应用程序源代码的命令
git clone --branch v10.1.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
让我们看一下演示应用程序的架构
1. products service
: 处理从数据库查询产品并将其返回给前端2. orders service
: 处理验证和创建订单3. order history service
: 处理查询客户的订单历史记录4. payments service
: 处理订单的支付处理5. api gateway
: 将服务统一到单个端点下6. mongodb/ postgresql
: 充当用于存储订单、订单历史记录、产品等的写入优化数据库电子商务微服务应用程序由一个前端组成,该前端使用 Next.js 与 TailwindCSS 构建。应用程序后端使用 Node.js。数据存储在 Redis 和 MongoDB 或 PostgreSQL 中,使用 Prisma。以下是展示电子商务应用程序前端的屏幕截图。
仪表盘: 显示产品列表,并具有不同的搜索功能,可在设置页面中进行配置。
地理位置搜索
,搜索栏支持位置查询。地理位置搜索涉及根据经纬度坐标标识的地理位置查询和处理数据。此功能对于各种应用程序至关重要,包括基于位置的服务、附近搜索和空间分析。
它允许系统在地理环境中存储、索引和快速检索数据点,例如查找距离特定点一定距离内的所有用户,或计算两个位置之间的距离等。
Redis 的 地理空间 功能使开发人员能够轻松构建具有位置感知功能的应用程序,这些应用程序可以执行附近搜索、基于位置的过滤和空间分析。
Redis 的 内存中 架构确保地理空间数据存储在内存中并进行处理,从而实现 低延迟 和基于位置的查询的高吞吐量。这使得 Redis 成为需要 实时 基于位置的搜索功能的应用程序的理想选择。
考虑一个多店购物场景,消费者在线上找到产品,在浏览器或移动设备中下订单,并在最近的商店位置提货。这被称为“在线购买,店内提货”(BOPIS)。Redis 使商店库存的 实时视图 和无缝的 BOPIS 购物体验成为可能。
我们的演示应用程序在 Redis 中使用两个主要数据集合来模拟电子商务平台的库存系统
1. products
集合:此集合存储有关每个产品的详细信息,包括名称、描述、类别和价格。2. storeInventory
集合:此集合维护跨不同商店位置的产品库存状态。它记录了每个产品在不同商店的可用数量,便于库存跟踪和管理。storeId
和相关的 stockQty
来标识。为了在我们的 storeInventory 集合中启用地理位置搜索,必须适当地索引数据。Redis 提供了多种创建索引的方法:使用命令行界面 (CLI) 或使用 Redis OM、node redis 等客户端库。
为了促进对 storeInventory
集合的地理空间查询和其他搜索操作,请执行以下命令
# Remove existing index
FT.DROPINDEX "storeInventory:storeInventoryId:index"
# Create a new index with geo-spatial and other field capabilities
FT.CREATE "storeInventory:storeInventoryId:index"
ON JSON
PREFIX 1 "storeInventory:storeInventoryId:"
SCHEMA
"$.storeId" AS "storeId" TAG SEPARATOR "|"
"$.storeName" AS "storeName" TEXT
"$.storeLocation" AS "storeLocation" GEO
"$.productId" AS "productId" TAG SEPARATOR "|"
"$.productDisplayName" AS "productDisplayName" TEXT
"$.stockQty" AS "stockQty" NUMERIC
"$.statusCode" AS "statusCode" NUMERIC
对于使用 Node.js 环境的应用程序,Redis OM 提供了一种优雅的、面向对象的映射方法来与 Redis 交互。以下是一个使用 Redis OM 设置索引的实现示例
// Import necessary Redis OM classes
import {
Schema as RedisSchema,
Repository as RedisRepository,
EntityId as RedisEntityId,
} from 'redis-om';
import { getNodeRedisClient } from '../utils/redis/redis-wrapper';
// Define a prefix for store inventory keys and the schema for indexing
const STORE_INVENTORY_KEY_PREFIX = 'storeInventory:storeInventoryId';
const schema = new RedisSchema(STORE_INVENTORY_KEY_PREFIX, {
storeId: { type: 'string', indexed: true },
storeName: { type: 'text', indexed: true },
storeLocation: { type: 'point', indexed: true }, // Uses longitude,latitude format
productId: { type: 'string', indexed: true },
productDisplayName: { type: 'text', indexed: true },
stockQty: { type: 'number', indexed: true },
statusCode: { type: 'number', indexed: true },
});
/*
A Repository is the main interface into Redis OM. It gives us the methods to read, write, and remove a specific Entity
*/
const getRepository = () => {
const redisClient = getNodeRedisClient();
const repository = new RedisRepository(schema, redisClient);
return repository;
};
/*
we need to create an index or we won't be able to search.
Redis OM uses hash to see if index needs to be recreated or not
*/
const createRedisIndex = async () => {
const repository = getRepository();
await repository.createIndex();
};
export {
getRepository,
createRedisIndex,
RedisEntityId,
STORE_INVENTORY_KEY_PREFIX,
};
import * as StoreInventoryRepo from '../../../common/models/store-inventory-repo';
app.listen(PORT, async () => {
//...
// Create index for store inventory on startup
await StoreInventoryRepo.createRedisIndex();
//...
});
数据索引后,您可以执行原始 Redis 查询以执行地理位置搜索和其他空间操作。以下两个示例查询演示了这些功能
纽约市
)的 50 英里
半径内查找产品(puma
)。它将地理空间搜索功能与文本搜索相结合,根据位置和产品名称过滤结果。FT.SEARCH "storeInventory:storeInventoryId:index" "( ( ( (@statusCode:[1 1]) (@stockQty:[(0 +inf]) ) (@storeLocation:[-73.968285 40.785091 50 mi]) ) (@productDisplayName:'puma') )"
FT.SEARCH
命令在 storeInventory:storeInventoryId:index
中执行搜索。它指定了一个由中心的经度和纬度以及 50 英里
半径定义的圆形区域。此外,它还根据可用性 (@stockQty:[(0 +inf)])
和 @statusCode
(表示活动状态 ([1 1])
)过滤产品,并与产品显示名称包含 puma
的匹配项相结合。FT.AGGREGATE "storeInventory:storeInventoryId:index"
"( ( ( (@statusCode:[1 1]) (@stockQty:[(0 +inf]) ) (@storeLocation:[-73.968285 40.785091 50 mi]) ) (@productDisplayName:'puma') )"
LOAD 6 "@storeId" "@storeName" "@storeLocation" "@productId" "@productDisplayName" "@stockQty"
APPLY "geodistance(@storeLocation, -73.968285, 40.785091)/1609"
AS "distInMiles"
SORTBY 1 "@distInMiles"
LIMIT 0 100
FT.AGGREGATE
用于处理和转换搜索结果。APPLY
子句计算每个商店位置与指定坐标之间的距离,并将结果转换为英里。SORTBY
子句按此距离对结果进行排序,而 LIMIT
将输出限制为 100 个条目,这使得该查询非常适合需要排序的基于接近度的搜索结果的应用程序。getStoreProductsByGeoFilter
API 端点使客户端能够根据地理位置和产品名称搜索商店产品,这展示了 Redis 地理位置搜索功能的实际应用。
API 请求
请求有效负载指定要搜索的产品名称、搜索半径(以英里为单位)以及用户的当前位置(以纬度和经度坐标表示)。
POST https://localhost:3000/products/getStoreProductsByGeoFilter
{
"productDisplayName":"puma",
"searchRadiusInMiles":50,
"userLocation": {
"latitude": 40.785091,
"longitude": -73.968285
}
}
API 响应
响应返回一个与搜索条件匹配的产品数组,包括有关每个产品及其与用户位置距离的详细信息。
{
"data": [
{
"productId": "11000",
"price": 3995,
"productDisplayName": "Puma Men Slick 3HD Yellow Black Watches",
"variantName": "Slick 3HD Yellow",
"brandName": "Puma",
"ageGroup": "Adults-Men",
"gender": "Men",
"displayCategories": "Accessories",
"masterCategory_typeName": "Accessories",
"subCategory_typeName": "Watches",
"styleImages_default_imageURL": "https://host.docker.internal:8080/images/11000.jpg",
"productDescriptors_description_value": "...",
"stockQty": "5",
"storeId": "11_NY_MELVILLE",
"storeLocation": {
"longitude": -73.41512,
"latitude": 40.79343
},
"distInMiles": "46.59194"
}
//...
],
"error": null
}
本节概述了 getStoreProductsByGeoFilter
API 的实现,重点关注执行核心搜索逻辑的 searchStoreInventoryByGeoFilter
函数。
1. 函数概述:searchStoreInventoryByGeoFilter
接受一个库存过滤对象,该对象包括可选的产品显示名称、搜索半径(以英里为单位)和用户位置。它构造一个查询,以查找在指定半径内并与产品名称匹配的商店产品。
2. 查询构建:该函数使用 Redis OM 的流畅 API 构建一个搜索查询,指定产品可用性、库存数量和与用户位置接近度的条件。如果指定,它还可以根据名称过滤产品。
3. 执行查询: 使用 ft.aggregate 方法对 Redis 执行构建的查询,该方法允许进行复杂的聚合和转换。 处理查询结果以计算用户位置的距离(以英里为单位),并根据距离对结果进行排序。
4. 结果处理: 该函数过滤掉不同商店中的重复产品,确保最终输出中显示唯一的产品列表。 然后,它将商店位置格式化为更易读的结构,并编译最终的产品列表以返回。
import * as StoreInventoryRepo from '../../../common/models/store-inventory-repo';
interface IInventoryBodyFilter {
productDisplayName?: string;
searchRadiusInMiles?: number;
userLocation?: {
latitude?: number;
longitude?: number;
},
}
const searchStoreInventoryByGeoFilter = async (
_inventoryFilter: IInventoryBodyFilter,
) => {
// (1) ---
const redisClient = getNodeRedisClient();
const repository = StoreInventoryRepo.getRepository();
let storeProducts: IStoreInventory[] = [];
const trimmedStoreProducts: IStoreInventory[] = [] // similar item of other stores are removed
const uniqueProductIds = {};
if (repository
&& _inventoryFilter?.userLocation?.latitude
&& _inventoryFilter?.userLocation?.longitude) {
const lat = _inventoryFilter.userLocation.latitude;
const long = _inventoryFilter.userLocation.longitude;
const radiusInMiles = _inventoryFilter.searchRadiusInMiles || 500;
// (2) --- Query Construction
let queryBuilder = repository
.search()
.and('statusCode')
.eq(1)
.and('stockQty')
.gt(0)
.and('storeLocation')
.inRadius((circle) => {
return circle
.latitude(lat)
.longitude(long)
.radius(radiusInMiles)
.miles
});
if (_inventoryFilter.productDisplayName) {
queryBuilder = queryBuilder
.and('productDisplayName')
.matches(_inventoryFilter.productDisplayName)
}
console.log(queryBuilder.query);
/* Sample queryBuilder.query to run on CLI
FT.SEARCH "storeInventory:storeInventoryId:index" "( ( ( (@statusCode:[1 1]) (@stockQty:[(0 +inf]) ) (@storeLocation:[-73.968285 40.785091 50 mi]) ) (@productDisplayName:'puma') )"
*/
// (3) --- Executing the Query
const indexName = `storeInventory:storeInventoryId:index`;
const aggregator = await redisClient.ft.aggregate(
indexName,
queryBuilder.query,
{
LOAD: ["@storeId", "@storeName", "@storeLocation", "@productId", "@productDisplayName", "@stockQty"],
STEPS: [{
type: AggregateSteps.APPLY,
expression: `geodistance(@storeLocation, ${long}, ${lat})/1609`,
AS: 'distInMiles'
}, {
type: AggregateSteps.SORTBY,
BY: ["@distInMiles", "@productId"]
}, {
type: AggregateSteps.LIMIT,
from: 0,
size: 1000,
}]
});
/* Sample command to run on CLI
FT.AGGREGATE "storeInventory:storeInventoryId:index"
"( ( ( (@statusCode:[1 1]) (@stockQty:[(0 +inf]) ) (@storeLocation:[-73.968285 40.785091 50 mi]) ) (@productDisplayName:'puma') )"
"LOAD" "6" "@storeId" "@storeName" "@storeLocation" "@productId" "@productDisplayName" "@stockQty"
"APPLY" "geodistance(@storeLocation, -73.968285, 40.785091)/1609"
"AS" "distInMiles"
"SORTBY" "1" "@distInMiles"
"LIMIT" "0" "100"
*/
storeProducts = <IStoreInventory[]>aggregator.results;
if (!storeProducts.length) {
// throw `Product not found with in ${radiusInMiles}mi range!`;
}
else {
// (4) --- Result Processing
storeProducts.forEach((storeProduct) => {
if (storeProduct?.productId && !uniqueProductIds[storeProduct.productId]) {
uniqueProductIds[storeProduct.productId] = true;
if (typeof storeProduct.storeLocation == "string") {
const location = storeProduct.storeLocation.split(",");
storeProduct.storeLocation = {
longitude: Number(location[0]),
latitude: Number(location[1]),
}
}
trimmedStoreProducts.push(storeProduct)
}
});
}
}
else {
throw "Mandatory fields like userLocation latitude / longitude missing !"
}
return {
storeProducts: trimmedStoreProducts,
productIds: Object.keys(uniqueProductIds)
};
};
此实现展示了 Redis 地理位置搜索功能的实际用例,展示了如何执行与其他过滤条件(如产品名称)相结合的邻近搜索,并将结果以用户友好的格式呈现。
确保在设置页面中选择 地理位置搜索
以启用该功能。
Redis 的地理位置搜索功能提供了一种强大且高效的方式来执行基于邻近度的查询和分析。
通过利用 Redis 的内存数据存储和专门的地理命令,开发人员可以构建可扩展、高性能的应用程序,这些应用程序可以快速响应基于位置的查询。 与 JavaScript 生态系统的集成进一步简化了开发过程,从而实现无缝的应用程序开发和部署。