diff --git a/model/redis_recommend.go b/model/redis_recommend.go index 0fbc100..6c2aa63 100644 --- a/model/redis_recommend.go +++ b/model/redis_recommend.go @@ -215,7 +215,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe 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 handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice) }, ) return finalizeResults(searchInput, results) @@ -261,7 +261,21 @@ func normalizeTrailingDotSearchInput(ctx context.Context, rdb *redis.Client, inp return input } - checks := exactMatchChecksForInput(strings.Split(trimmedInput, ".")) + inputSlice := strings.Split(trimmedInput, ".") + if check, ok := completeMeasurementExactMatchCheck(inputSlice); ok { + exists, err := rdb.SIsMember(ctx, check.setKey, check.member).Result() + if err != nil { + logger.Warn(ctx, "check complete measurement with trailing dot failed", "input", input, "key", check.setKey, "member", check.member, "error", err) + return input + } + if exists { + // A measurement is terminal. Its trailing dot cannot enter another + // hierarchy level, so search the complete measurement without it. + return trimmedInput + } + } + + checks := exactMatchChecksForInput(inputSlice) if len(checks) == 0 { return input } @@ -279,6 +293,30 @@ func normalizeTrailingDotSearchInput(ctx context.Context, rdb *redis.Client, inp return trimmedInput } +// completeMeasurementExactMatchCheck returns the authoritative Redis lookup +// for each supported complete measurement identity shape. +func completeMeasurementExactMatchCheck(inputSlice []string) (exactMatchCheck, bool) { + switch len(inputSlice) { + case 7: // token1.token2.token3.token4.token5.token6.token7 + return exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, inputSlice[4]), + member: inputSlice[6], + }, true + case 4: // token4.token5.token6.token7 + return exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, inputSlice[1]), + member: inputSlice[3], + }, true + case 2: // token4.token7 + return exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, inputSlice[0]), + member: inputSlice[1], + }, true + default: + return exactMatchCheck{}, false + } +} + type exactMatchCheck struct { setKey string member string @@ -1087,9 +1125,10 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co } } - keyExists, err := rdb.SIsMember(ctx, redisSetKey, searchInput).Result() + exactSetKey := exactSearchRedisSetKey(hierarchy, inputSlice, redisSetKey) + keyExists, err := rdb.SIsMember(ctx, exactSetKey, searchInput).Result() if err != nil { - logger.Error(ctx, "check key exist from redis set failed ", "key", redisSetKey, "error", err) + logger.Error(ctx, "check key exist from redis set failed ", "key", exactSetKey, "error", err) return SearchResult{ RecommendType: hierarchy, QueryDatas: nil, @@ -1153,6 +1192,16 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co } } +// exactSearchRedisSetKey uses the component-specific measurement set for +// token7, ensuring an exact hit belongs to the component in the input path. +func exactSearchRedisSetKey(hierarchy constants.RecommendHierarchyType, inputSlice []string, defaultSetKey string) string { + if hierarchy == constants.MeasTagRecommendHierarchyType && len(inputSlice) >= 3 { + compTag := inputSlice[len(inputSlice)-3] + return fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag) + } + return defaultSetKey +} + // 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 @@ -1189,7 +1238,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string, } if len(results) == 0 { if fuzzyExactContinuationExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, searchInput, fuzzyInput) { - return []string{"."}, fuzzyRecommendOffset(searchPrefix, searchInput), nil + return []string{fuzzyExactContinuationValue(hierarchy)}, fuzzyRecommendOffset(searchPrefix, searchInput), nil } // 如果没有结果,退一步(删除最后一个字符)并继续循环 diff --git a/model/redis_recommend_test.go b/model/redis_recommend_test.go index c386a10..c559b4d 100644 --- a/model/redis_recommend_test.go +++ b/model/redis_recommend_test.go @@ -56,6 +56,43 @@ func TestFuzzyRecommendMemberSetKeyUsesLocalNSPathSet(t *testing.T) { } } +func TestMeasurementExactSearchUsesComponentSpecificSet(t *testing.T) { + tests := []struct { + name string + inputSlice []string + want string + }{ + { + name: "token1 through token7", + inputSlice: []string{"grid", "zone", "station", "nspath", "comp_tag", "bay", "measurement"}, + want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), + }, + { + name: "token4 through token7", + inputSlice: []string{"nspath", "comp_tag", "bay", "measurement"}, + want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := exactSearchRedisSetKey(constants.MeasTagRecommendHierarchyType, tt.inputSlice, constants.RedisAllMeasTagSetKey) + if got != tt.want { + t.Fatalf("expected exact search set %q, got %q", tt.want, got) + } + }) + } +} + +func TestFuzzyExactContinuationValueTreatsMeasurementAsTerminal(t *testing.T) { + if got := fuzzyExactContinuationValue(constants.MeasTagRecommendHierarchyType); got != "" { + t.Fatalf("expected empty completion for measurement, got %q", got) + } + if got := fuzzyExactContinuationValue(constants.CompNSPathRecommendHierarchyType); got != "." { + t.Fatalf("expected level continuation for component nspath, got %q", got) + } +} + func TestNormalizeRecommendResultsKeepsFuzzyExactContinuation(t *testing.T) { tests := []struct { name string @@ -772,6 +809,60 @@ func TestExactMatchChecksForInput(t *testing.T) { } } +func TestCompleteMeasurementTrailingDotChecks(t *testing.T) { + tests := []struct { + name string + input string + wantOffset int + wantCheck exactMatchCheck + }{ + { + name: "token1 through token7", + input: "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay.IA_rms_CTA-testProject1.", + wantOffset: 105, + wantCheck: exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "cable_22-testProject1110kV_TV"), + member: "IA_rms_CTA-testProject1", + }, + }, + { + name: "token4 through token7", + input: "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay.IA_rms_CTA-testProject1.", + wantOffset: 78, + wantCheck: exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "cable_22-testProject1110kV_TV"), + member: "IA_rms_CTA-testProject1", + }, + }, + { + name: "token4 and token7", + input: "110kV_TV-testProject1.IA_rms_CTA-testProject1.", + wantOffset: 45, + wantCheck: exactMatchCheck{ + setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "110kV_TV-testProject1"), + member: "IA_rms_CTA-testProject1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + measurement := strings.TrimSuffix(tt.input, ".") + if got := len([]rune(measurement)); got != tt.wantOffset { + t.Fatalf("expected offset %d, got %d", tt.wantOffset, got) + } + + got, ok := completeMeasurementExactMatchCheck(strings.Split(measurement, ".")) + if !ok { + t.Fatal("expected complete measurement exact-match check") + } + if !reflect.DeepEqual(got, tt.wantCheck) { + t.Fatalf("expected check %#v, got %#v", tt.wantCheck, got) + } + }) + } +} + func TestFallbackSpecificSetKey(t *testing.T) { tests := []struct { name string