fix(recommend): handle trailing dots on complete measurements
- normalize redundant trailing dots for supported measurement formats - validate exact measurement matches against component-specific Redis sets - prevent terminal measurement matches from returning dot continuations - add regression tests for offsets and exact completions
This commit is contained in:
parent
78f68b9c4f
commit
8195200cea
|
|
@ -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
|
||||
}
|
||||
|
||||
// 如果没有结果,退一步(删除最后一个字符)并继续循环
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue