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)
|
return handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompNSPathSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
func() SearchResult {
|
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)
|
return finalizeResults(searchInput, results)
|
||||||
|
|
@ -261,7 +261,21 @@ func normalizeTrailingDotSearchInput(ctx context.Context, rdb *redis.Client, inp
|
||||||
return input
|
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 {
|
if len(checks) == 0 {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
@ -279,6 +293,30 @@ func normalizeTrailingDotSearchInput(ctx context.Context, rdb *redis.Client, inp
|
||||||
return trimmedInput
|
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 {
|
type exactMatchCheck struct {
|
||||||
setKey string
|
setKey string
|
||||||
member 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 {
|
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{
|
return SearchResult{
|
||||||
RecommendType: hierarchy,
|
RecommendType: hierarchy,
|
||||||
QueryDatas: nil,
|
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 校验候选项确属当前层级。
|
// runFuzzySearch 执行 RediSearch 模糊搜索, 并用 Redis set 校验候选项确属当前层级。
|
||||||
func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType, recommendLenType string) ([]string, int, error) {
|
func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType, recommendLenType string) ([]string, int, error) {
|
||||||
var configToken string
|
var configToken string
|
||||||
|
|
@ -1189,7 +1238,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
||||||
}
|
}
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
if fuzzyExactContinuationExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, searchInput, fuzzyInput) {
|
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) {
|
func TestNormalizeRecommendResultsKeepsFuzzyExactContinuation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
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) {
|
func TestFallbackSpecificSetKey(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue