You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 11:43:27