C# Redis哈希集优化:无需全量拉取实现住宿搜索
无需拉取全量Redis哈希数据实现住宿信息搜索的方案
问题背景
我有一个Demo项目,使用Redis缓存存储数据库中的大量住宿数据,采用Hash Set(哈希集)存储。当前UI搜索组件在用户输入内容时,会从Redis拉取所有哈希数据,映射为GetModel后再执行本地搜索。相关C#代码如下:
public async Task<bool> AddAccommodationsHash() { var cacheKey = GenerateCacheKey("Accommodation"); var accommodations = await _service.GetAllAccommodations(); foreach (var accommodation in accommodations) { db.HashSet(cacheKey, new HashEntry[] { new HashEntry(accommodation.AccommodationCode, accommodation.AccommodationName) }); } return true; } public async Task<IEnumerable<AccommodationGetModel>> FindAccommodations(string searchQuery) { var hashEntries = await AccommodationHashGetOrSet(key); var result = hashEntries .Where(e => $"{e.Name}-".Contains(searchQuery, StringComparison.OrdinalIgnoreCase) || $"-{e.Value}".Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) .OrderBy(e => { var firstIndex = $"{e.Name}-{e.Value}".IndexOf(searchQuery, StringComparison.OrdinalIgnoreCase); return firstIndex < 0 ? 1 : firstIndex; }) .ThenBy(e => $"{e.Name}-{e.Value}"); return MapAccommodationsToAccommodationGetModel(result); }
现在需要实现无需从Redis拉取全量数据就能完成住宿信息的搜索。
可行解决方案
方案1:使用Redis哈希SCAN命令(渐进式遍历+过滤)
Redis的HSCAN命令支持渐进式遍历哈希表,无需一次性拉取全量数据,同时可通过通配符做初步匹配,减少传输到客户端的数据量。在StackExchange.Redis中可通过HashScanAsync实现:
public async Task<IEnumerable<AccommodationGetModel>> FindAccommodationsWithScan(string searchQuery) { var cacheKey = GenerateCacheKey("Accommodation"); var matchedEntries = new List<HashEntry>(); var cursor = 0L; do { // 渐进式扫描哈希表,每次返回一批匹配通配符的条目 var scanResult = await db.HashScanAsync(cacheKey, cursor, match: $"*{searchQuery}*", pageSize: 100); cursor = scanResult.Cursor; // 对返回结果做更精确的大小写不敏感匹配 matchedEntries.AddRange(scanResult.Where(e => $"{e.Name}-{e.Value}".IndexOf(searchQuery, StringComparison.OrdinalIgnoreCase) >= 0)); } while (cursor != 0); // 按匹配位置排序后映射为GetModel var sortedResults = matchedEntries .OrderBy(e => { var firstIndex = $"{e.Name}-{e.Value}".IndexOf(searchQuery, StringComparison.OrdinalIgnoreCase); return firstIndex < 0 ? 1 : firstIndex; }) .ThenBy(e => $"{e.Name}-{e.Value}"); return MapAccommodationsToAccommodationGetModel(sortedResults); }
注意:HSCAN的match参数仅基于哈希键(e.Name)做通配符匹配,若需匹配哈希值(e.Value),仍需客户端二次过滤,但相比全量拉取,数据传输量已大幅降低。
方案2:基于RedisSearch模块构建全文索引(最优复杂搜索方案)
如果你的Redis部署了RedisSearch(RediSearch)模块,可以为住宿数据构建全文搜索索引,直接在Redis端完成匹配、排序、分页等操作,完全无需拉取全量数据。
步骤1:创建搜索索引
public async Task CreateAccommodationSearchIndex() { var indexName = "idx:accommodations"; // 检查索引是否存在,不存在则创建 if (!await db.ExecuteAsync("FT._LIST").ToStringArray().Contains(indexName)) { await db.ExecuteAsync("FT.CREATE", indexName, "ON", "HASH", "PREFIX", "1", GenerateCacheKey("Accommodation"), // 指定要索引的哈希键前缀 "SCHEMA", "AccommodationCode", "TEXT", "SORTABLE", // 住宿编码设为可搜索、可排序字段 "AccommodationName", "TEXT", "SORTABLE"); // 住宿名称设为可搜索、可排序字段 } }
步骤2:同步数据与索引
修改数据写入逻辑,确保新增/更新住宿数据时自动同步索引:
public async Task<bool> AddAccommodationsHash() { var cacheKey = GenerateCacheKey("Accommodation"); var accommodations = await _service.GetAllAccommodations(); foreach (var accommodation in accommodations) { // 存储哈希数据,RedisSearch会自动同步索引 await db.HashSetAsync(cacheKey, new HashEntry[] { new HashEntry("AccommodationCode", accommodation.AccommodationCode), new HashEntry("AccommodationName", accommodation.AccommodationName) }); } await CreateAccommodationSearchIndex(); return true; }
步骤3:执行全文搜索
public async Task<IEnumerable<AccommodationGetModel>> FindAccommodationsWithSearch(string searchQuery) { var indexName = "idx:accommodations"; // 调用RedisSearch命令,在Redis端完成匹配、排序 var searchResult = await db.ExecuteAsync("FT.SEARCH", indexName, $"{searchQuery}", // 全文搜索词 "SORTBY", "AccommodationName", "ASC", // 按住宿名称升序排序 "LIMIT", 0, 50); // 限制返回数量,避免大量数据传输 // 解析Redis返回的搜索结果 var results = new List<AccommodationGetModel>(); if (searchResult.Type == ResultType.MultiBulk) { var bulkResult = (RedisResult[])searchResult; // 第一个元素是匹配总数,后续为哈希键和对应字段集合 for (int i = 1; i < bulkResult.Length; i += 2) { var hashKey = bulkResult[i].ToString(); var hashEntries = await db.HashGetAllAsync(hashKey); results.Add(MapHashToAccommodationGetModel(hashEntries)); } } return results; }
该方案适合需要复杂搜索(模糊匹配、多字段组合、分页)的场景,性能最优。
方案3:预构建前缀索引(针对前缀搜索场景)
如果搜索需求以前缀匹配为主(如输入"Bei"匹配"北京XX酒店"),可提前为住宿名称、编码构建前缀索引,用Redis的Set存储前缀对应的住宿编码:
步骤1:构建前缀索引
public async Task BuildPrefixIndexes() { var cacheKey = GenerateCacheKey("Accommodation"); var accommodations = await _service.GetAllAccommodations(); foreach (var acc in accommodations) { // 为住宿名称构建所有可能的前缀索引 var lowerName = acc.AccommodationName.ToLower(); for (int i = 1; i <= lowerName.Length; i++) { var prefix = lowerName.Substring(0, i); await db.SetAddAsync($"prefix:name:{prefix}", acc.AccommodationCode); } // 为住宿编码构建前缀索引 var lowerCode = acc.AccommodationCode.ToLower(); for (int i = 1; i <= lowerCode.Length; i++) { var prefix = lowerCode.Substring(0, i); await db.SetAddAsync($"prefix:code:{prefix}", acc.AccommodationCode); } } }
步骤2:通过前缀索引查询
public async Task<IEnumerable<AccommodationGetModel>> FindAccommodationsWithPrefix(string searchQuery) { var lowerQuery = searchQuery.ToLower(); var nameIndexKey = $"prefix:name:{lowerQuery}"; var codeIndexKey = $"prefix:code:{lowerQuery}"; // 获取匹配前缀的所有住宿编码 var matchingCodes = await db.SetUnionAsync(nameIndexKey, codeIndexKey); if (!matchingCodes.Any()) return Enumerable.Empty<AccommodationGetModel>(); // 批量拉取匹配的哈希数据(仅拉取需要的条目) var cacheKey = GenerateCacheKey("Accommodation"); var hashValues = await db.HashGetAsync(cacheKey, matchingCodes.Select(c => (RedisValue)c).ToArray()); // 映射并排序 var matchedEntries = new List<HashEntry>(); for (int i = 0; i < matchingCodes.Length; i++) { if (hashValues[i].HasValue) { matchedEntries.Add(new HashEntry(matchingCodes[i], hashValues[i])); } } var sortedResults = matchedEntries .OrderBy(e => { var firstIndex = $"{e.Name}-{e.Value}".IndexOf(searchQuery, StringComparison.OrdinalIgnoreCase); return firstIndex < 0 ? 1 : firstIndex; }) .ThenBy(e => $"{e.Name}-{e.Value}"); return MapAccommodationsToAccommodationGetModel(sortedResults); }
该方案前缀搜索速度快,但需要额外存储空间维护索引,且数据更新时需同步更新索引。
内容的提问来源于stack exchange,提问作者Muhammet Fatih Özata
相关产品推荐
相关产品推荐

