// Package model define model struct of model runtime service package model import ( "context" "errors" "fmt" "math" "slices" "strings" "sync" "modelRT/constants" "modelRT/diagram" "modelRT/logger" "modelRT/util" "github.com/RediSearch/redisearch-go/v2/redisearch" redigo "github.com/gomodule/redigo/redis" "github.com/redis/go-redis/v9" "golang.org/x/sync/errgroup" ) // SearchResult define struct to store redis query recommend search result type SearchResult struct { // input redis key, used to distinguish which goroutine the result belongs to RecommendType constants.RecommendHierarchyType QueryDatas []string IsFuzzy bool // 代表本次搜索是否是模糊搜索, 例如输入 grid1. IsFallback bool Offset int Err error } var ac *redisearch.Autocompleter // InitAutocompleterWithPool define func of initialize the Autocompleter with redigo pool func InitAutocompleterWithPool(pool *redigo.Pool) { ac = redisearch.NewAutocompleterFromPool(pool, constants.RedisSearchDictName) } type recommendSearchJob func() SearchResult func runRecommendSearches(ctx context.Context, logMsg string, filterCompNSPath bool, jobs ...recommendSearchJob) map[string]SearchResult { results := make(map[string]SearchResult) var mu sync.Mutex var group errgroup.Group for _, job := range jobs { job := job group.Go(func() error { result := job() if result.Err != nil { return fmt.Errorf("%s: %w", result.RecommendType, result.Err) } if filterCompNSPath && result.RecommendType == constants.CompNSPathRecommendHierarchyType { result.QueryDatas = filterLocalNSPathResults(result.QueryDatas) } mu.Lock() results[result.RecommendType.String()] = result mu.Unlock() return nil }) } if err := group.Wait(); err != nil { logger.Error(ctx, logMsg, "error", err) } return results } // levelOneRedisSearch 处理第一段输入的搜索, 例如 grid 或本地 nspath。 func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, searchInput string, searchRedisKey string) (result SearchResult) { defer func() { if r := recover(); r != nil { logger.Error(ctx, "searchFunc panicked", "panic", r) result = SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: errors.New("search goroutine panicked"), } } }() exists, err := rdb.SIsMember(ctx, searchRedisKey, searchInput).Result() if err != nil { logger.Error(ctx, "redis membership check failed", "key", searchRedisKey, "member", searchInput, "op", "SIsMember", "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } // the input key is the complete hierarchical value if exists { return SearchResult{ RecommendType: hierarchy, QueryDatas: []string{"."}, IsFuzzy: false, Err: nil, } } // process fuzzy search result recommends, offset, err := runFuzzySearch(ctx, rdb, searchInput, "", hierarchy, recommendLenType) if err != nil { logger.Error(ctx, fmt.Sprintf("fuzzy search failed for %s hierarchical", util.GetLevelStrByRdsKey(searchRedisKey)), "search_input", searchInput, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } if len(recommends) > 0 { return SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, Offset: offset, Err: nil, } } recommends, err = fallbackAllCurrentLevel(ctx, rdb, hierarchy) if err != nil { logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: true, IsFallback: true, Err: err, } } return SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, IsFallback: true, Offset: 0, Err: nil, } } // RedisSearchRecommend define func of redis search by input string and return recommend results func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchResult { rdb := diagram.GetRedisClientInstance() finalizeResults := func(normalizeInput string, results map[string]SearchResult) map[string]SearchResult { results = suppressFallbackResultsWhenConcreteHitExists(results) return normalizeRecommendResults(normalizeInput, results) } if input == "" || shouldFallbackToInitialRecommend(input) { results := runRecommendSearches(ctx, "return all keys at the special level from redis failed", true, func() SearchResult { return getAllKeyByGridLevel(ctx, rdb) }, func() SearchResult { return getAllKeyByNSPathLevel(ctx, rdb) }, ) return finalizeResults("", results) } inputSlice := strings.Split(input, ".") inputSliceLen := len(inputSlice) switch inputSliceLen { case 1: searchInput := inputSlice[0] results := runRecommendSearches(ctx, "exec redis fuzzy search by key failed", true, func() SearchResult { return levelOneRedisSearch(ctx, rdb, constants.GridRecommendHierarchyType, constants.FullRecommendLength, searchInput, constants.RedisAllGridSetKey) }, func() SearchResult { return levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, searchInput, constants.RedisAllCompNSPathSetKey) }, ) return finalizeResults(input, results) case 2: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllZoneSetKey, inputSlice) }, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllCompTagSetKey, inputSlice) }, func() SearchResult { return handleNSPathMeasSearch(ctx, rdb, inputSlice) }, ) return finalizeResults(input, results) case 3: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.StationRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllStationSetKey, inputSlice) }, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice) }, ) return finalizeResults(input, results) case 4: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompNSPathSetKey, inputSlice) }, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice) }, ) return finalizeResults(input, results) case 5: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice) }, ) return finalizeResults(input, results) case 6: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice) }, ) return finalizeResults(input, results) case 7: results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false, func() SearchResult { return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice) }, ) return finalizeResults(input, results) default: logger.Error(ctx, "unsupport length of search input", "input_len", inputSliceLen) return nil } } func shouldFallbackToInitialRecommend(input string) bool { return strings.HasPrefix(input, ".") } // suppressFallbackResultsWhenConcreteHitExists 有明确命中时丢弃兜底结果, 避免返回过宽推荐。 func suppressFallbackResultsWhenConcreteHitExists(results map[string]SearchResult) map[string]SearchResult { hasConcreteHit := false for _, result := range results { if result.Err == nil && !result.IsFallback && len(result.QueryDatas) > 0 { hasConcreteHit = true break } } if !hasConcreteHit { return results } for key, result := range results { if result.IsFallback { delete(results, key) } } return results } // normalizeRecommendResults 统一处理 offset、去重和裁剪, 让响应只返回用户还没输入的部分。 func normalizeRecommendResults(input string, results map[string]SearchResult) map[string]SearchResult { for key, result := range results { if result.Err != nil { continue } if !result.IsFuzzy { if hasExactCompletion(result.QueryDatas) { result.Offset = len([]rune(input)) } else if hasLevelContinuation(result.QueryDatas) { result.Offset = len([]rune(input)) results[key] = result continue } else { result.Offset = calcSearchResultOffset(input, result.QueryDatas) } } result.QueryDatas = trimRecommendTerms(result.QueryDatas, result.Offset) results[key] = result } return results } // hasExactCompletion 判断推荐项是否表示当前输入已经完整命中, 无需再补充后缀。 func hasExactCompletion(recommends []string) bool { return slices.Contains(recommends, "") } // hasLevelContinuation 判断推荐项是否表示当前层级已命中, 可继续补充分隔符进入下一层。 func hasLevelContinuation(recommends []string) bool { return slices.Contains(recommends, ".") } // calcSearchResultOffset 计算 input 与推荐项的最短公共前缀长度, 用作裁剪位置。 func calcSearchResultOffset(input string, recommends []string) int { var minOffset int for index, recommend := range recommends { offset := util.GetLongestCommonPrefixLength(input, recommend) if index == 0 || offset < minOffset { minOffset = offset } } return minOffset } // recommendPrefixOffset 返回推荐结果中已由输入确定的前缀长度, 包含末尾分隔符。 func recommendPrefixOffset(prefixTokens []string) int { prefix := strings.Join(prefixTokens, ".") if prefix == "" { return 0 } return len([]rune(prefix)) + 1 } // trimRecommendTerms 按 offset 截掉已输入前缀, 并去掉空值和重复值。 func trimRecommendTerms(recommends []string, offset int) []string { resultRecommends := make([]string, 0, len(recommends)) seen := make(map[string]struct{}) for _, recommend := range recommends { recommendRunes := []rune(recommend) if offset > len(recommendRunes) { continue } recommendTerm := string(recommendRunes[offset:]) if recommendTerm == "" { continue } if _, exists := seen[recommendTerm]; exists { continue } seen[recommendTerm] = struct{}{} resultRecommends = append(resultRecommends, recommendTerm) } return resultRecommends } // filterLocalNSPathResults 只保留本地组件 nspath, 避免远端 nspath 进入本地简写推荐。 func filterLocalNSPathResults(queryDatas []string) []string { filterResults := make([]string, 0, len(queryDatas)) for _, queryData := range queryDatas { if queryData == "." { filterResults = append(filterResults, queryData) continue } var nsPath string if lastDotIndex := strings.LastIndex(queryData, "."); lastDotIndex == -1 { nsPath = queryData } else { nsPath = queryData[lastDotIndex+1:] } // TODO 考虑为 NSPathToIsLocalMap 增加读写锁或改为并发安全快照, 保证过滤数据一致性 if isLocal, ok := NSPathToIsLocalMap[nsPath]; ok && isLocal { filterResults = append(filterResults, queryData) } } return filterResults } // queryMemberFromSpecificsLevel 根据上一层级 token 查询其真实下级集合。 func queryMemberFromSpecificsLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, keyPrefix string) ([]string, error) { queryKey := getSpecificKeyByLength(hierarchy, keyPrefix) return rdb.SMembers(ctx, queryKey).Result() } // fallbackAllCurrentLevel 兜底返回当前层级的全量集合。 func fallbackAllCurrentLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType) ([]string, error) { setKey, ok := getAllSetKeyByHierarchy(hierarchy) if !ok { return nil, fmt.Errorf("unsupported recommend hierarchy for fallback: %s", hierarchy) } members, err := rdb.SMembers(ctx, setKey).Result() if err != nil && err != redis.Nil { return nil, err } return members, nil } // fallbackCurrentLevelByInput 兜底时先校验上下级关系, 避免错误前缀返回全层级数据。 func fallbackCurrentLevelByInput(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, inputSlice []string) ([]string, error) { allSetKey, ok := getAllSetKeyByHierarchy(hierarchy) if !ok { return nil, fmt.Errorf("unsupported recommend hierarchy for fallback: %s", hierarchy) } specificSetKey, ok := fallbackSpecificSetKey(hierarchy, inputSlice) if !ok { return fallbackAllCurrentLevel(ctx, rdb, hierarchy) } count, err := rdb.SInterCard(ctx, 1, allSetKey, specificSetKey).Result() if err != nil && err != redis.Nil { return nil, err } if count == 0 { return []string{}, nil } members, err := rdb.SMembers(ctx, specificSetKey).Result() if err != nil && err != redis.Nil { return nil, err } return members, nil } // fallbackSpecificSetKey 根据输入切片推导 fallback 应该校验的上一层级 specific set。 func fallbackSpecificSetKey(hierarchy constants.RecommendHierarchyType, inputSlice []string) (string, bool) { if len(inputSlice) == 0 { return "", false } searchInputIndex := len(inputSlice) - 1 specificKeyIndex := searchInputIndex - 1 if hierarchy == constants.MeasTagRecommendHierarchyType { specificKeyIndex = searchInputIndex - 2 } if specificKeyIndex < 0 || specificKeyIndex >= len(inputSlice) || inputSlice[specificKeyIndex] == "" { return "", false } switch hierarchy { case constants.ZoneRecommendHierarchyType, constants.StationRecommendHierarchyType, constants.CompNSPathRecommendHierarchyType, constants.CompTagRecommendHierarchyType, constants.MeasTagRecommendHierarchyType: return getSpecificKeyByLength(hierarchy, inputSlice[specificKeyIndex]), true default: return "", false } } // getAllSetKeyByHierarchy 返回某个推荐层级对应的全量 Redis set key。 func getAllSetKeyByHierarchy(hierarchy constants.RecommendHierarchyType) (string, bool) { switch hierarchy { case constants.GridRecommendHierarchyType: return constants.RedisAllGridSetKey, true case constants.ZoneRecommendHierarchyType: return constants.RedisAllZoneSetKey, true case constants.StationRecommendHierarchyType: return constants.RedisAllStationSetKey, true case constants.CompNSPathRecommendHierarchyType: return constants.RedisAllCompNSPathSetKey, true case constants.CompTagRecommendHierarchyType: return constants.RedisAllCompTagSetKey, true case constants.ConfigRecommendHierarchyType: return constants.RedisAllConfigSetKey, true case constants.MeasTagRecommendHierarchyType: return constants.RedisAllMeasTagSetKey, true default: return "", false } } // getAllKeyByGridLevel 返回所有 grid tag, 用于空输入时的初始推荐。 func getAllKeyByGridLevel(ctx context.Context, rdb *redis.Client) SearchResult { setKey := constants.RedisAllGridSetKey hierarchy := constants.GridRecommendHierarchyType members, err := rdb.SMembers(ctx, setKey).Result() if err != nil && err != redigo.ErrNil { logger.Error(ctx, "get all members from redis by special key failed", "key", setKey, "op", "SMembers", "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } return SearchResult{ RecommendType: hierarchy, QueryDatas: members, IsFuzzy: false, Err: nil, } } // getAllKeyByNSPathLevel 返回所有组件 nspath, 用于空输入时的初始推荐。 func getAllKeyByNSPathLevel(ctx context.Context, rdb *redis.Client) SearchResult { queryKey := constants.RedisAllCompNSPathSetKey hierarchy := constants.CompNSPathRecommendHierarchyType members, err := rdb.SMembers(ctx, queryKey).Result() if err != nil && err != redigo.ErrNil { logger.Error(ctx, "get all members by special key failed", "key", queryKey, "op", "SMembers", "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } return SearchResult{ RecommendType: hierarchy, QueryDatas: members, IsFuzzy: false, Err: nil, } } // combineQueryResultByInput 把当前层级查询结果和输入前缀拼成完整推荐路径。 func combineQueryResultByInput(hierarchy constants.RecommendHierarchyType, recommendLenType string, inputSlice []string, queryResults []string) []string { prefixs := make([]string, 0, len(inputSlice)) recommandResults := make([]string, 0, len(queryResults)) switch recommendLenType { case constants.FullRecommendLength: switch hierarchy { case constants.ZoneRecommendHierarchyType: prefixs = []string{inputSlice[0]} case constants.StationRecommendHierarchyType: prefixs = inputSlice[0:2] case constants.CompNSPathRecommendHierarchyType: prefixs = inputSlice[0:3] case constants.CompTagRecommendHierarchyType: prefixs = inputSlice[0:4] case constants.ConfigRecommendHierarchyType: prefixs = inputSlice[0:5] case constants.MeasTagRecommendHierarchyType: prefixs = inputSlice[0:6] default: return []string{} } case constants.IsLocalRecommendLength: switch hierarchy { case constants.CompTagRecommendHierarchyType: prefixs = []string{inputSlice[0]} case constants.ConfigRecommendHierarchyType: prefixs = inputSlice[0:2] case constants.MeasTagRecommendHierarchyType: prefixs = inputSlice[0:3] default: return []string{} } } for _, queryResult := range queryResults { combineStrs := make([]string, 0, len(inputSlice)) combineStrs = append(combineStrs, prefixs...) combineStrs = append(combineStrs, queryResult) recommandResult := strings.Join(combineStrs, ".") recommandResults = append(recommandResults, recommandResult) } return recommandResults } // confirmMeasRecommendResultsByHash 用 token5-token6 hash 确认 token7 是否属于当前配置组。 func confirmMeasRecommendResultsByHash(ctx context.Context, rdb *redis.Client, inputSlice []string, members []string, recommends []string) []string { compTag, configToken, ok := recommendGroupTokens(inputSlice) if !ok { return []string{} } hashKey := recommendGroupKey(compTag, configToken) confirmedResults := make([]string, 0, len(recommends)) for index, recommend := range recommends { if index >= len(members) { break } exists, err := rdb.HExists(ctx, hashKey, members[index]).Result() if err != nil { logger.Error(ctx, "measurement recommend hash confirmation failed", "hash_key", hashKey, "field", members[index], "error", err) continue } if !exists { continue } confirmedResults = append(confirmedResults, recommend) } return confirmedResults } func recommendGroupTokens(inputSlice []string) (string, string, bool) { searchInputIndex := len(inputSlice) - 1 compTagIndex := searchInputIndex - 2 configTokenIndex := searchInputIndex - 1 if compTagIndex < 0 || configTokenIndex < 0 || configTokenIndex >= len(inputSlice) { return "", "", false } compTag := inputSlice[compTagIndex] configToken := inputSlice[configTokenIndex] return compTag, configToken, compTag != "" && configToken != "" } // getSpecificKeyByLength 根据层级和父级 token 生成对应的 specific set key。 func getSpecificKeyByLength(hierarchy constants.RecommendHierarchyType, keyPrefix string) string { switch hierarchy { case constants.GridRecommendHierarchyType: return constants.RedisAllGridSetKey case constants.ZoneRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, keyPrefix) case constants.StationRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, keyPrefix) case constants.CompNSPathRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, keyPrefix) case constants.CompTagRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, keyPrefix) case constants.ConfigRecommendHierarchyType: return constants.RedisAllConfigSetKey case constants.MeasTagRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, keyPrefix) default: return constants.RedisAllGridSetKey } } // handleNSPathMeasSearch 处理 nspath.measurement 的简写搜索路径。 func handleNSPathMeasSearch(ctx context.Context, rdb *redis.Client, inputSlice []string) SearchResult { hierarchy := constants.MeasTagRecommendHierarchyType nsPath := inputSlice[0] searchInput := inputSlice[1] nsPathExists, err := rdb.SIsMember(ctx, constants.RedisAllCompNSPathSetKey, nsPath).Result() if err != nil { logger.Error(ctx, "check nspath exist from redis set failed", "key", constants.RedisAllCompNSPathSetKey, "nspath", nsPath, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } if !nsPathExists { return SearchResult{ RecommendType: hierarchy, QueryDatas: []string{}, IsFuzzy: false, Err: nil, } } queryKey := fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, nsPath) if searchInput == "" { members, err := rdb.SMembers(ctx, queryKey).Result() if err != nil && err != redis.Nil { logger.Error(ctx, "query measurement tags by nspath failed", "key", queryKey, "nspath", nsPath, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } return SearchResult{ RecommendType: hierarchy, QueryDatas: combineNSPathMeasResults(nsPath, members), IsFuzzy: false, Err: nil, } } measTagExists, err := rdb.SIsMember(ctx, queryKey, searchInput).Result() if err != nil { logger.Error(ctx, "check measurement tag exist from nspath set failed", "key", queryKey, "meas_tag", searchInput, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } if measTagExists { return SearchResult{ RecommendType: hierarchy, QueryDatas: []string{""}, IsFuzzy: false, Err: nil, } } recommends, offset, err := runNSPathMeasFuzzySearch(ctx, nsPath, searchInput) if err != nil { return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: true, Err: err, } } isFallback := false if len(recommends) == 0 { queryKey := fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, nsPath) members, err := rdb.SMembers(ctx, queryKey).Result() if err != nil && err != redis.Nil { logger.Error(ctx, "fallback measurement tags by nspath failed", "key", queryKey, "nspath", nsPath, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: true, IsFallback: true, Err: err, } } recommends = combineNSPathMeasResults(nsPath, members) offset = recommendPrefixOffset([]string{nsPath}) isFallback = true } return SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, IsFallback: isFallback, Offset: offset, Err: nil, } } // combineNSPathMeasResults 把 nspath 与 measurement tag 拼成简写推荐结果。 func combineNSPathMeasResults(nsPath string, measTags []string) []string { results := make([]string, 0, len(measTags)) for _, measTag := range measTags { results = append(results, fmt.Sprintf("%s.%s", nsPath, measTag)) } return results } // runNSPathMeasFuzzySearch 对 nspath.measurement 简写做模糊搜索, 无结果时逐字回退。 func runNSPathMeasFuzzySearch(ctx context.Context, nsPath string, searchInput string) ([]string, int, error) { searchInputLen := len([]rune(searchInput)) for searchInputLen != 0 { fuzzyInput := fmt.Sprintf("%s.%s", nsPath, searchInput) results, err := ac.SuggestOpts(fuzzyInput, redisearch.SuggestOptions{ Num: math.MaxInt16, Fuzzy: true, WithScores: false, WithPayloads: false, }) if err != nil { logger.Error(ctx, "query measurement tag by nspath fuzzy search failed", "query_key", fuzzyInput, "error", err) return nil, 0, fmt.Errorf("redisearch suggest failed: %w", err) } if len(results) == 0 { searchInput = trimLastRune(searchInput) searchInputLen = len([]rune(searchInput)) continue } recommends := make([]string, 0, len(results)) for _, result := range results { termPrefix, termLastPart := splitLastToken(result.Term) if termPrefix == nsPath && strings.HasPrefix(termLastPart, searchInput) { recommends = append(recommends, result.Term) } } if len(recommends) == 0 { searchInput = trimLastRune(searchInput) searchInputLen = len([]rune(searchInput)) continue } return recommends, len([]rune(fmt.Sprintf("%s.%s", nsPath, searchInput))), nil } return []string{}, 0, nil } // splitLastToken 拆分最后一个点号后的 token, 返回前缀和末级 token。 func splitLastToken(term string) (string, string) { lastDotIndex := strings.LastIndex(term, ".") if lastDotIndex == -1 { return "", term } return term[:lastDotIndex], term[lastDotIndex+1:] } // handleLevelFuzzySearch 处理第二层及以后层级的精确、模糊和兜底推荐。 func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, redisSetKey string, inputSlice []string) SearchResult { inputSliceLen := len(inputSlice) searchInputIndex := inputSliceLen - 1 searchInput := inputSlice[searchInputIndex] searchPrefix := strings.Join(inputSlice[0:searchInputIndex], ".") if searchInput == "" { var specificalKey string specificalKeyIndex := searchInputIndex - 1 if hierarchy == constants.MeasTagRecommendHierarchyType { specificalKeyIndex = searchInputIndex - 2 } if specificalKeyIndex >= 0 { specificalKey = inputSlice[specificalKeyIndex] } if recommendLenType == constants.IsLocalRecommendLength && hierarchy == constants.ConfigRecommendHierarchyType { // token4-token7 model and query all config keys redisSetKey = constants.RedisAllCompTagSetKey keyExists, err := rdb.SIsMember(ctx, redisSetKey, specificalKey).Result() if err != nil { logger.Error(ctx, "check key exist from redis set failed ", "key", redisSetKey, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } if !keyExists { return SearchResult{ RecommendType: hierarchy, QueryDatas: []string{}, IsFuzzy: false, Err: nil, } } } members, err := queryMemberFromSpecificsLevel(ctx, rdb, hierarchy, specificalKey) if err != nil && err != redis.Nil { logger.Error(ctx, "query members from redis by special key failed", "key", specificalKey, "member", searchInput, "op", "SMember", "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } recommandResults := combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, members) if hierarchy == constants.MeasTagRecommendHierarchyType { recommandResults = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, members, recommandResults) } if hierarchy == constants.ConfigRecommendHierarchyType { // check the relevance between the config hierarchy and measurement hierarchy output and the request input, i.e., use FT.SUGGET search_suggestions_dict "recommandResult" max 1 for an exact query to check if the result exists secondConfirmResults := make([]string, 0, len(recommandResults)) for _, res := range recommandResults { results, err := ac.SuggestOpts(res, redisearch.SuggestOptions{ Num: 1, Fuzzy: false, WithScores: false, WithPayloads: false, }) if err != nil { logger.Error(ctx, "config hierarchy query key second confirmation failed", "query_key", res, "error", err) continue } if len(results) == 0 { continue } secondConfirmResults = append(secondConfirmResults, res) } recommandResults = secondConfirmResults } return SearchResult{ RecommendType: hierarchy, QueryDatas: recommandResults, IsFuzzy: false, Err: nil, } } keyExists, err := rdb.SIsMember(ctx, redisSetKey, searchInput).Result() if err != nil { logger.Error(ctx, "check key exist from redis set failed ", "key", redisSetKey, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } if keyExists { var QueryData []string if hierarchy == constants.MaxIdentifyHierarchy || (hierarchy == constants.IdentifyHierarchy && redisSetKey == constants.RedisAllMeasTagSetKey) { QueryData = []string{""} } else { QueryData = []string{"."} } return SearchResult{ RecommendType: hierarchy, QueryDatas: QueryData, IsFuzzy: false, Err: nil, } } // start redis fuzzy search recommends, offset, err := runFuzzySearch(ctx, rdb, searchInput, searchPrefix, hierarchy, recommendLenType) if err != nil { logger.Error(ctx, "fuzzy search failed by hierarchy", "hierarchy", hierarchy, "search_input", searchInput, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } } isFallback := false if len(recommends) == 0 { logger.Info(ctx, "fuzzy search without result", "hierarchy", hierarchy, "search_input", searchInput, "error", err) recommends, err = fallbackCurrentLevelByInput(ctx, rdb, hierarchy, inputSlice) if err != nil { logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: true, IsFallback: true, Err: err, } } isFallback = true if _, ok := fallbackSpecificSetKey(hierarchy, inputSlice); ok { recommends = combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, recommends) offset = recommendPrefixOffset(inputSlice[:len(inputSlice)-1]) } else { offset = 0 } } return SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, IsFallback: isFallback, Offset: offset, Err: nil, } } // runFuzzySearch 执行 RediSearch 模糊搜索, 并用 Redis set 校验候选项确属当前层级。 func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType, recommendLenType string) ([]string, int, error) { var configToken string var comparePrefix string var memberComparePrefix string searchInputLen := len([]rune(searchInput)) compareHierarchyLen := int(hierarchy) comparePrefix = searchPrefix memberComparePrefix = searchPrefix if hierarchy == constants.MeasTagRecommendHierarchyType { lastDotIndex := strings.LastIndex(searchPrefix, ".") if lastDotIndex == -1 { configToken = "" } else { configToken = searchPrefix[lastDotIndex+1:] memberComparePrefix = searchPrefix[:lastDotIndex] } } for searchInputLen != 0 { fuzzyInput := searchInput if comparePrefix != "" { fuzzyInput = comparePrefix + "." + searchInput } results, err := ac.SuggestOpts(fuzzyInput, redisearch.SuggestOptions{ Num: math.MaxInt16, Fuzzy: true, WithScores: false, WithPayloads: false, }) if err != nil { logger.Error(ctx, "query key by redis fuzzy search failed", "query_key", fuzzyInput, "error", err) return nil, 0, fmt.Errorf("redisearch suggest failed: %w", err) } if len(results) == 0 { // 如果没有结果,退一步(删除最后一个字符)并继续循环 // TODO 考虑使用其他方式代替 for 循环退一字符的查询方式 searchInput = trimLastRune(searchInput) searchInputLen = len([]rune(searchInput)) continue } var recommends []string for _, result := range results { term := result.Term var termHierarchyLen int var termPrefix string var termLastPart string lastDotIndex := strings.LastIndex(term, ".") if lastDotIndex == -1 { termPrefix = "" termLastPart = term } else { termPrefix = term[:lastDotIndex] termLastPart = term[lastDotIndex+1:] } switch recommendLenType { case constants.FullRecommendLength: if result.Term == "" { termHierarchyLen = 1 } else { termHierarchyLen = strings.Count(result.Term, ".") + 1 } case constants.IsLocalRecommendLength: if result.Term == "" { termHierarchyLen = 4 } else { termHierarchyLen = strings.Count(result.Term, ".") + 4 } } if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings.HasPrefix(termLastPart, searchInput) { exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, memberComparePrefix, termLastPart) if err != nil || !exists { logger.Info(ctx, "fuzzy recommend term not found in specific redis set", "hierarchy", hierarchy, "recommend_len_type", recommendLenType, "query_key", fuzzyInput, "term", term, "term_last_part", termLastPart, "exists", exists, "error", err, ) continue } recommend := result.Term if hierarchy == constants.MeasTagRecommendHierarchyType && configToken != "" && termPrefix == memberComparePrefix { recommend = strings.Join([]string{termPrefix, configToken, termLastPart}, ".") } recommends = append(recommends, recommend) } } if len(recommends) == 0 { searchInput = trimLastRune(searchInput) searchInputLen = len([]rune(searchInput)) continue } return recommends, fuzzyRecommendOffset(searchPrefix, searchInput), nil } return []string{}, 0, nil } // fuzzyRecommendOffset 返回模糊命中的裁剪位置, 保留完整输入前缀参与 offset。 func fuzzyRecommendOffset(searchPrefix string, searchInput string) int { if searchPrefix == "" { return len([]rune(searchInput)) } return len([]rune(searchPrefix)) + 1 + len([]rune(searchInput)) } // fuzzyRecommendMemberExists 校验模糊候选末级 token 是否存在于对应 Redis set。 func fuzzyRecommendMemberExists(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, comparePrefix string, termLastPart string) (bool, error) { setKey, ok := fuzzyRecommendMemberSetKey(hierarchy, comparePrefix) if !ok { return true, nil } return rdb.SIsMember(ctx, setKey, termLastPart).Result() } // fuzzyRecommendMemberSetKey 返回模糊候选校验时应使用的 Redis set key。 func fuzzyRecommendMemberSetKey(hierarchy constants.RecommendHierarchyType, comparePrefix string) (string, bool) { switch hierarchy { case constants.GridRecommendHierarchyType: return constants.RedisAllGridSetKey, true case constants.ZoneRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, comparePrefix), comparePrefix != "" case constants.StationRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != "" case constants.CompNSPathRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != "" case constants.CompTagRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != "" case constants.ConfigRecommendHierarchyType: return constants.RedisAllConfigSetKey, true case constants.MeasTagRecommendHierarchyType: return fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != "" default: return "", false } } // lastRecommendPrefixToken 取推荐前缀中的最后一个层级 token。 func lastRecommendPrefixToken(prefix string) string { if lastDotIndex := strings.LastIndex(prefix, "."); lastDotIndex != -1 { return prefix[lastDotIndex+1:] } return prefix } // trimLastRune 删除字符串最后一个 rune, 用于模糊搜索逐字回退。 func trimLastRune(s string) string { runes := []rune(s) if len(runes) == 0 { return s } return string(runes[:len(runes)-1]) }