// Package model define model struct of model runtime service package model import ( "context" "errors" "fmt" "math" "strings" "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" ) // 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 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) } func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, searchInput string, searchRedisKey string, fanInChan chan SearchResult) { defer func() { if r := recover(); r != nil { logger.Error(ctx, "searchFunc panicked", "panic", r) fanInChan <- 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) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } // the input key is the complete hierarchical value if exists { fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: []string{"."}, IsFuzzy: false, Err: nil, } return } // process fuzzy search result recommends, err := runFuzzySearch(ctx, searchInput, "", hierarchy) if err != nil { logger.Error(ctx, fmt.Sprintf("fuzzy search failed for %s hierarchical", util.GetLevelStrByRdsKey(searchRedisKey)), "search_input", searchInput, "error", err) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } if len(recommends) > 0 { fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, Err: nil, } return } fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: []string{}, IsFuzzy: true, 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() if input == "" { fanInChan := make(chan SearchResult, 2) // return all grid tagname go getAllKeyByGridLevel(ctx, rdb, fanInChan) // return all component nspath go getAllKeyByNSPathLevel(ctx, rdb, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "return all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } if result.RecommendType == constants.CompNSPathRecommendHierarchyType { // TODO 增加 nspath 过滤 fmt.Println("process nspath") } results[result.RecommendType.String()] = result } return results } inputSlice := strings.Split(input, ".") inputSliceLen := len(inputSlice) fanInChan := make(chan SearchResult, 4) switch inputSliceLen { case 1: searchInput := inputSlice[0] // grid tagname search go levelOneRedisSearch(ctx, rdb, constants.GridRecommendHierarchyType, searchInput, constants.RedisAllGridSetKey, fanInChan) // component nspath search go levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, searchInput, constants.RedisAllCompNSPathSetKey, fanInChan) results := make(map[string]SearchResult) // TODO 后续根据支持的数据标识语法长度,进行值的变更 for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "exec redis fuzzy search by key :%s failed", "query_key", result.RecommendType, "error", result.Err) continue } if result.RecommendType == constants.CompNSPathRecommendHierarchyType { // TODO 增加 nspath 过滤 fmt.Println("process nspath") } results[result.RecommendType.String()] = result } return results case 2: // zone tagname search go handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.RedisAllZoneSetKey, inputSlice, fanInChan) // component tagname search go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.RedisAllCompTagSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results case 3: // station tanname search go handleLevelFuzzySearch(ctx, rdb, constants.StationRecommendHierarchyType, constants.RedisAllStationSetKey, inputSlice, fanInChan) // config search go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.RedisAllConfigSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results case 4: // component nspath search go handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.RedisAllCompNSPathSetKey, inputSlice, fanInChan) // measurement tagname search go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.RedisAllConfigSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results case 5: // component tagname search go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.RedisAllCompTagSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results case 6: // config search go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.RedisAllConfigSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results case 7: // measurement tagname search go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.RedisAllMeasTagSetKey, inputSlice, fanInChan) results := make(map[string]SearchResult) for range 2 { result := <-fanInChan if result.Err != nil { logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err) continue } results[result.RecommendType.String()] = result } return results default: logger.Error(ctx, "unsupport length of search input", "input_len", inputSliceLen) return nil } } 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() } func getAllKeyByGridLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) { queryKey := constants.RedisAllGridSetKey hierarchy := constants.GridRecommendHierarchyType members, err := rdb.SMembers(ctx, queryKey).Result() if err != nil { logger.Error(ctx, "get all members by special key failed", "key", queryKey, "op", "SMembers", "error", err) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: members, IsFuzzy: false, Err: nil, } } func getAllKeyByNSPathLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) { queryKey := constants.RedisAllCompNSPathSetKey hierarchy := constants.CompNSPathRecommendHierarchyType members, err := rdb.SMembers(ctx, queryKey).Result() if err != nil { logger.Error(ctx, "get all members by special key failed", "key", queryKey, "op", "SMembers", "error", err) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: members, IsFuzzy: false, Err: nil, } } func combineQueryResultByInput(hierarchy constants.RecommendHierarchyType, inputSlice []string, queryResults []string) []string { prefixs := make([]string, 0, len(inputSlice)) recommandResults := make([]string, 0, len(queryResults)) switch hierarchy { // TODO 优化 case 为常量 case 2: prefixs = []string{inputSlice[0]} case 3: prefixs = inputSlice[0:2] case 4: prefixs = inputSlice[0:3] case 5: prefixs = inputSlice[0:4] case 6: prefixs = inputSlice[0:5] case 7: prefixs = inputSlice[0:6] 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 } func getSpecificKeyByLength(hierarchy constants.RecommendHierarchyType, keyPrefix string) string { switch hierarchy { // TODO 优化 case 为常量 case 1: return constants.RedisAllGridSetKey case 2: return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, keyPrefix) case 3: return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, keyPrefix) case 4: return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, keyPrefix) case 5: return fmt.Sprintf(constants.RedisSpecStationCompTagSetKey, keyPrefix) case 6: return constants.RedisAllConfigSetKey case 7: return fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, keyPrefix) default: return constants.RedisAllGridSetKey } } // handleLevelFuzzySearch define func to process recommendation logic for specific levels(level >= 2) func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, redisSetKey string, inputSlice []string, fanInChan chan 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 specificalKeyIndex >= 0 { specificalKey = inputSlice[specificalKeyIndex] } members, err := queryMemberFromSpecificsLevel(ctx, rdb, hierarchy, specificalKey) if err != nil { logger.Error(ctx, "query members from redis by special key failed", "key", specificalKey, "member", searchInput, "op", "SMember", "error", err) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } recommandResults := combineQueryResultByInput(hierarchy, inputSlice, members) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: recommandResults, IsFuzzy: false, Err: nil, } return } 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) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } if keyExists { var QueryData []string if hierarchy == constants.MaxIdentifyHierarchy || (hierarchy == constants.IdentifyHierarchy && redisSetKey == constants.RedisAllMeasTagSetKey) { QueryData = []string{""} } else { QueryData = []string{"."} } fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: QueryData, IsFuzzy: false, Err: nil, } return } // start redis fuzzy search recommends, err := runFuzzySearch(ctx, searchInput, searchPrefix, hierarchy) if err != nil { logger.Error(ctx, "fuzzy search failed by hierarchy", "hierarchy", hierarchy, "search_input", searchInput, "error", err) fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: nil, IsFuzzy: false, Err: err, } return } if len(recommends) == 0 { logger.Error(ctx, "fuzzy search without result", "hierarchy", hierarchy, "search_input", searchInput, "error", err) } fanInChan <- SearchResult{ RecommendType: hierarchy, QueryDatas: recommends, IsFuzzy: true, Err: nil, } return } // runFuzzySearch define func to process redis fuzzy search func runFuzzySearch(ctx context.Context, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType) ([]string, error) { searchInputLen := len(searchInput) for searchInputLen != 0 { results, err := ac.SuggestOpts(searchInput, 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", searchInput, "error", err) return nil, fmt.Errorf("redisearch suggest failed: %w", err) } if len(results) == 0 { // 如果没有结果,退一步(删除最后一个字节)并继续循环 // TODO 考虑使用其他方式代替 for 循环退一字节的查询方式 searchInput = searchInput[:len(searchInput)-1] searchInputLen = len(searchInput) continue } var recommends []string for _, result := range results { term := result.Term var termSliceLen int var termPrefix string lastDotIndex := strings.LastIndex(term, ".") if lastDotIndex == -1 { termPrefix = "" } else { termPrefix = term[:lastDotIndex] } if result.Term == "" { termSliceLen = 1 } else { termSliceLen = strings.Count(result.Term, ".") + 1 } if termSliceLen == int(hierarchy) && termPrefix == searchPrefix { recommends = append(recommends, result.Term) } } return recommends, nil } return []string{}, nil }