fix: correct measurement recommendation fuzzy matching and hierarchy fallback
- Fix component tag suggestion writes where full paths were overwritten by local paths - Add exact token checks for trailing-dot inputs to avoid trimming valid hierarchy levels - Improve config/meas fallback by trimming the input prefix before filtering candidates, avoiding full-level fallback output - Use hash-based validation for config and measurement recommendation ownership instead of long-prefix autocomplete exact queries
This commit is contained in:
parent
6f78d8e341
commit
578f805b57
|
|
@ -22,6 +22,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
|
|
||||||
compTagToFullPath := make(map[string]string)
|
compTagToFullPath := make(map[string]string)
|
||||||
isLocalCompTagToFullPath := make(map[string]string)
|
isLocalCompTagToFullPath := make(map[string]string)
|
||||||
|
var allErrs []error
|
||||||
|
|
||||||
zoneToGridPath := make(map[string]string)
|
zoneToGridPath := make(map[string]string)
|
||||||
for gridTag, zoneTags := range measSet.GridToZoneTags {
|
for gridTag, zoneTags := range measSet.GridToZoneTags {
|
||||||
|
|
@ -62,12 +63,21 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
pipe.SAdd(ctx, key, members)
|
pipe.SAdd(ctx, key, members)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
safeAddTerms := func(sug []redisearch.Suggestion) {
|
||||||
|
if len(sug) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ac.AddTerms(sug...); err != nil {
|
||||||
|
logger.Error(ctx, "add measurement group recommend suggestions failed", "error", err)
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
safeSAdd(constants.RedisAllGridSetKey, measSet.AllGridTags)
|
safeSAdd(constants.RedisAllGridSetKey, measSet.AllGridTags)
|
||||||
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
|
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
|
||||||
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
|
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
|
||||||
})
|
})
|
||||||
ac.AddTerms(gridSug...)
|
safeAddTerms(gridSug)
|
||||||
|
|
||||||
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
|
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
|
||||||
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
|
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
|
||||||
|
|
@ -83,7 +93,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
|
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
|
||||||
})
|
})
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the zone -> stations hierarchy
|
// building the zone -> stations hierarchy
|
||||||
|
|
@ -101,7 +111,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
})
|
})
|
||||||
|
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the station -> component nspaths hierarchy
|
// building the station -> component nspaths hierarchy
|
||||||
|
|
@ -122,7 +132,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
|
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the component nspath -> component tags hierarchy
|
// building the component nspath -> component tags hierarchy
|
||||||
|
|
@ -136,20 +146,17 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, compTag := range compTags {
|
for _, compTag := range compTags {
|
||||||
fullPath := fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag)
|
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
|
||||||
compTagToFullPath[compTag] = fullPath
|
compTagToFullPath[compTag] = fullTerm
|
||||||
fullPath = fmt.Sprintf("%s.%s", compNSPath, compTag)
|
isLocalCompTagToFullPath[compTag] = localTerm
|
||||||
isLocalCompTagToFullPath[compTag] = fullPath
|
|
||||||
|
|
||||||
// add redis fuzzy search suggestion for token1-token7 type
|
// add redis fuzzy search suggestion for token1-token7 type
|
||||||
term := fullPath
|
sug = append(sug, redisearch.Suggestion{Term: fullTerm, Score: constants.DefaultScore})
|
||||||
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
|
|
||||||
// add redis fuzzy search suggestion for token4-token7 type
|
// add redis fuzzy search suggestion for token4-token7 type
|
||||||
term = fmt.Sprintf("%s.%s", compNSPath, compTag)
|
sug = append(sug, redisearch.Suggestion{Term: localTerm, Score: constants.DefaultScore})
|
||||||
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
|
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the component tag -> measurement tags hierarchy
|
// building the component tag -> measurement tags hierarchy
|
||||||
|
|
@ -187,7 +194,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
if len(measTags) > 0 {
|
if len(measTags) > 0 {
|
||||||
pipe.HSet(ctx, recommendGroupKey(compTag, "bay"), recommendHashFields(measTags)...)
|
pipe.HSet(ctx, recommendGroupKey(compTag, "bay"), recommendHashFields(measTags)...)
|
||||||
}
|
}
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the component nspath -> measurement tags hierarchy for token4-token7 shorthand
|
// building the component nspath -> measurement tags hierarchy for token4-token7 shorthand
|
||||||
|
|
@ -200,10 +207,9 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, compNSPath), measTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, compNSPath), measTags)
|
||||||
ac.AddTerms(sug...)
|
safeAddTerms(sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var allErrs []error
|
|
||||||
cmders, execErr := pipe.Exec(ctx)
|
cmders, execErr := pipe.Exec(ctx)
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
logger.Error(ctx, "pipeline execution failed", "error", execErr)
|
logger.Error(ctx, "pipeline execution failed", "error", execErr)
|
||||||
|
|
@ -227,3 +233,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
|
|
||||||
return compTagToFullPath, isLocalCompTagToFullPath, nil
|
return compTagToFullPath, isLocalCompTagToFullPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compTagSuggestionTerms(parentPath string, compNSPath string, compTag string) (string, string) {
|
||||||
|
return fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag), fmt.Sprintf("%s.%s", compNSPath, compTag)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCompTagSuggestionTermsKeepFullAndLocalPaths(t *testing.T) {
|
||||||
|
parentPath := "grid000.zone000.station000"
|
||||||
|
compNSPath := "110kV_TV-demoProject"
|
||||||
|
compTag := "cable_22-testProject1110kV_TV"
|
||||||
|
|
||||||
|
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
|
||||||
|
|
||||||
|
wantFullTerm := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
||||||
|
if fullTerm != wantFullTerm {
|
||||||
|
t.Fatalf("expected full suggestion term %q, got %q", wantFullTerm, fullTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLocalTerm := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
||||||
|
if localTerm != wantLocalTerm {
|
||||||
|
t.Fatalf("expected local suggestion term %q, got %q", wantLocalTerm, localTerm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -171,9 +171,9 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
|
||||||
return finalizeResults("", results)
|
return finalizeResults("", results)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputSlice := strings.Split(input, ".")
|
searchInput := normalizeTrailingDotSearchInput(ctx, rdb, input)
|
||||||
|
inputSlice := strings.Split(searchInput, ".")
|
||||||
inputSliceLen := len(inputSlice)
|
inputSliceLen := len(inputSlice)
|
||||||
|
|
||||||
switch inputSliceLen {
|
switch inputSliceLen {
|
||||||
case 1:
|
case 1:
|
||||||
searchInput := inputSlice[0]
|
searchInput := inputSlice[0]
|
||||||
|
|
@ -185,7 +185,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
|
||||||
return levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, searchInput, constants.RedisAllCompNSPathSetKey)
|
return levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, searchInput, constants.RedisAllCompNSPathSetKey)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 2:
|
case 2:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
|
|
@ -198,7 +198,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
|
||||||
return handleNSPathMeasSearch(ctx, rdb, inputSlice)
|
return handleNSPathMeasSearch(ctx, rdb, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 3:
|
case 3:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
|
|
@ -208,7 +208,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
|
||||||
return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 4:
|
case 4:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
|
|
@ -218,28 +218,28 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
|
||||||
return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 5:
|
case 5:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
return handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice)
|
return handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 6:
|
case 6:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
return handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
case 7:
|
case 7:
|
||||||
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
results := runRecommendSearches(ctx, "query all keys at the special level from redis failed", false,
|
||||||
func() SearchResult {
|
func() SearchResult {
|
||||||
return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice)
|
return handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return finalizeResults(input, results)
|
return finalizeResults(searchInput, results)
|
||||||
default:
|
default:
|
||||||
logger.Error(ctx, "unsupport length of search input", "input_len", inputSliceLen)
|
logger.Error(ctx, "unsupport length of search input", "input_len", inputSliceLen)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -250,6 +250,92 @@ func shouldFallbackToInitialRecommend(input string) bool {
|
||||||
return strings.HasPrefix(input, ".")
|
return strings.HasPrefix(input, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeTrailingDotSearchInput 忽略误输入的尾随点号, 让前一段 token 继续参与模糊搜索。
|
||||||
|
func normalizeTrailingDotSearchInput(ctx context.Context, rdb *redis.Client, input string) string {
|
||||||
|
if input == "" || !strings.HasSuffix(input, ".") || strings.HasSuffix(input, "..") {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedInput := strings.TrimSuffix(input, ".")
|
||||||
|
if trimmedInput == "" {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
checks := exactMatchChecksForInput(strings.Split(trimmedInput, "."))
|
||||||
|
if len(checks) == 0 {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
exists, err := rdb.SIsMember(ctx, check.setKey, check.member).Result()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(ctx, "check trailing dot input exact match failed", "input", input, "key", check.setKey, "member", check.member, "error", err)
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
type exactMatchCheck struct {
|
||||||
|
setKey string
|
||||||
|
member string
|
||||||
|
}
|
||||||
|
|
||||||
|
func exactMatchChecksForInput(inputSlice []string) []exactMatchCheck {
|
||||||
|
inputSliceLen := len(inputSlice)
|
||||||
|
if inputSliceLen == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastToken := inputSlice[inputSliceLen-1]
|
||||||
|
if lastToken == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch inputSliceLen {
|
||||||
|
case 1:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: constants.RedisAllGridSetKey, member: lastToken},
|
||||||
|
{setKey: constants.RedisAllCompNSPathSetKey, member: lastToken},
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, inputSlice[0]), member: lastToken},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, inputSlice[0]), member: lastToken},
|
||||||
|
{setKey: constants.RedisAllCompTagSetKey, member: lastToken},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, inputSlice[0]), member: lastToken},
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecZoneStationSetKey, inputSlice[1]), member: lastToken},
|
||||||
|
{setKey: constants.RedisAllConfigSetKey, member: lastToken},
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, inputSlice[2]), member: lastToken},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, inputSlice[1]), member: lastToken},
|
||||||
|
}
|
||||||
|
case 5:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, inputSlice[3]), member: lastToken},
|
||||||
|
{setKey: constants.RedisAllCompTagSetKey, member: lastToken},
|
||||||
|
}
|
||||||
|
case 6:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: constants.RedisAllConfigSetKey, member: lastToken},
|
||||||
|
}
|
||||||
|
case 7:
|
||||||
|
return []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, inputSlice[4]), member: lastToken},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// suppressFallbackResultsWhenConcreteHitExists 有明确命中时丢弃兜底结果, 避免返回过宽推荐。
|
// suppressFallbackResultsWhenConcreteHitExists 有明确命中时丢弃兜底结果, 避免返回过宽推荐。
|
||||||
func suppressFallbackResultsWhenConcreteHitExists(results map[string]SearchResult) map[string]SearchResult {
|
func suppressFallbackResultsWhenConcreteHitExists(results map[string]SearchResult) map[string]SearchResult {
|
||||||
hasConcreteHit := false
|
hasConcreteHit := false
|
||||||
|
|
@ -396,30 +482,101 @@ func fallbackAllCurrentLevel(ctx context.Context, rdb *redis.Client, hierarchy c
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallbackCurrentLevelByInput 兜底时先校验上下级关系, 避免错误前缀返回全层级数据。
|
// fallbackCurrentLevelByInput 兜底时先校验上下级关系, 避免错误前缀返回全层级数据。
|
||||||
func fallbackCurrentLevelByInput(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, inputSlice []string) ([]string, error) {
|
func fallbackCurrentLevelByInput(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, inputSlice []string) ([]string, int, error) {
|
||||||
allSetKey, ok := getAllSetKeyByHierarchy(hierarchy)
|
allSetKey, ok := getAllSetKeyByHierarchy(hierarchy)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unsupported recommend hierarchy for fallback: %s", hierarchy)
|
return nil, 0, fmt.Errorf("unsupported recommend hierarchy for fallback: %s", hierarchy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hierarchy == constants.ConfigRecommendHierarchyType {
|
||||||
|
return fallbackConfigByInput(ctx, rdb, allSetKey, recommendLenType, inputSlice)
|
||||||
|
}
|
||||||
|
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
||||||
|
return fallbackMeasByInput(ctx, rdb, allSetKey, recommendLenType, inputSlice)
|
||||||
}
|
}
|
||||||
|
|
||||||
specificSetKey, ok := fallbackSpecificSetKey(hierarchy, inputSlice)
|
specificSetKey, ok := fallbackSpecificSetKey(hierarchy, inputSlice)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fallbackAllCurrentLevel(ctx, rdb, hierarchy)
|
members, err := fallbackAllCurrentLevel(ctx, rdb, hierarchy)
|
||||||
|
return members, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := rdb.SInterCard(ctx, 1, allSetKey, specificSetKey).Result()
|
count, err := rdb.SInterCard(ctx, 1, allSetKey, specificSetKey).Result()
|
||||||
if err != nil && err != redis.Nil {
|
if err != nil && err != redis.Nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return []string{}, nil
|
return []string{}, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
members, err := rdb.SMembers(ctx, specificSetKey).Result()
|
members, err := rdb.SMembers(ctx, specificSetKey).Result()
|
||||||
if err != nil && err != redis.Nil {
|
if err != nil && err != redis.Nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return members, nil
|
return combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, members), recommendPrefixOffset(inputSlice[:len(inputSlice)-1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallbackConfigByInput(ctx context.Context, rdb *redis.Client, allSetKey string, recommendLenType string, inputSlice []string) ([]string, int, error) {
|
||||||
|
if len(inputSlice) < 2 {
|
||||||
|
members, err := fallbackAllCurrentLevel(ctx, rdb, constants.ConfigRecommendHierarchyType)
|
||||||
|
return members, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := rdb.SMembers(ctx, allSetKey).Result()
|
||||||
|
if err != nil && err != redis.Nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput := inputSlice[len(inputSlice)-1]
|
||||||
|
matchedMembers, matchedInput := filterMembersByTrimmedPrefix(members, searchInput)
|
||||||
|
if len(matchedMembers) == 0 {
|
||||||
|
return []string{}, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixTokens := inputSlice[:len(inputSlice)-1]
|
||||||
|
recommends := combineQueryResultByInput(constants.ConfigRecommendHierarchyType, recommendLenType, inputSlice, matchedMembers)
|
||||||
|
recommends = confirmConfigRecommendResultsByHash(ctx, rdb, inputSlice, matchedMembers, recommends)
|
||||||
|
if len(recommends) == 0 {
|
||||||
|
return []string{}, 0, nil
|
||||||
|
}
|
||||||
|
return recommends, fuzzyRecommendOffset(strings.Join(prefixTokens, "."), matchedInput), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallbackMeasByInput(ctx context.Context, rdb *redis.Client, allSetKey string, recommendLenType string, inputSlice []string) ([]string, int, error) {
|
||||||
|
members, err := rdb.SMembers(ctx, allSetKey).Result()
|
||||||
|
if err != nil && err != redis.Nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput := inputSlice[len(inputSlice)-1]
|
||||||
|
matchedMembers, matchedInput := filterMembersByTrimmedPrefix(members, searchInput)
|
||||||
|
if len(matchedMembers) == 0 {
|
||||||
|
return []string{}, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recommends := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, recommendLenType, inputSlice, matchedMembers)
|
||||||
|
recommends = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, matchedMembers, recommends)
|
||||||
|
if len(recommends) == 0 {
|
||||||
|
return []string{}, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommends, fuzzyRecommendOffset(strings.Join(inputSlice[:len(inputSlice)-1], "."), matchedInput), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterMembersByTrimmedPrefix(members []string, searchInput string) ([]string, string) {
|
||||||
|
for len([]rune(searchInput)) != 0 {
|
||||||
|
filtered := make([]string, 0, len(members))
|
||||||
|
for _, member := range members {
|
||||||
|
if strings.HasPrefix(member, searchInput) {
|
||||||
|
filtered = append(filtered, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
return filtered, searchInput
|
||||||
|
}
|
||||||
|
searchInput = trimLastRune(searchInput)
|
||||||
|
}
|
||||||
|
return []string{}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallbackSpecificSetKey 根据输入切片推导 fallback 应该校验的上一层级 specific set。
|
// fallbackSpecificSetKey 根据输入切片推导 fallback 应该校验的上一层级 specific set。
|
||||||
|
|
@ -591,6 +748,42 @@ func confirmMeasRecommendResultsByHash(ctx context.Context, rdb *redis.Client, i
|
||||||
return confirmedResults
|
return confirmedResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirmConfigRecommendResultsByHash(ctx context.Context, rdb *redis.Client, inputSlice []string, members []string, recommends []string) []string {
|
||||||
|
compTag, ok := configRecommendCompTag(inputSlice)
|
||||||
|
if !ok {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmedResults := make([]string, 0, len(recommends))
|
||||||
|
for index, recommend := range recommends {
|
||||||
|
if index >= len(members) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
hashKey := recommendGroupKey(compTag, members[index])
|
||||||
|
exists, err := rdb.Exists(ctx, hashKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "config recommend hash confirmation failed", "hash_key", hashKey, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if exists == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
confirmedResults = append(confirmedResults, recommend)
|
||||||
|
}
|
||||||
|
return confirmedResults
|
||||||
|
}
|
||||||
|
|
||||||
|
func configRecommendCompTag(inputSlice []string) (string, bool) {
|
||||||
|
configTokenIndex := len(inputSlice) - 1
|
||||||
|
compTagIndex := configTokenIndex - 1
|
||||||
|
if compTagIndex < 0 || compTagIndex >= len(inputSlice) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
compTag := inputSlice[compTagIndex]
|
||||||
|
return compTag, compTag != ""
|
||||||
|
}
|
||||||
|
|
||||||
func recommendGroupTokens(inputSlice []string) (string, string, bool) {
|
func recommendGroupTokens(inputSlice []string) (string, string, bool) {
|
||||||
searchInputIndex := len(inputSlice) - 1
|
searchInputIndex := len(inputSlice) - 1
|
||||||
compTagIndex := searchInputIndex - 2
|
compTagIndex := searchInputIndex - 2
|
||||||
|
|
@ -844,26 +1037,7 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
||||||
recommandResults = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, members, recommandResults)
|
recommandResults = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, members, recommandResults)
|
||||||
}
|
}
|
||||||
if hierarchy == constants.ConfigRecommendHierarchyType {
|
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
|
recommandResults = confirmConfigRecommendResultsByHash(ctx, rdb, inputSlice, members, recommandResults)
|
||||||
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{
|
return SearchResult{
|
||||||
RecommendType: hierarchy,
|
RecommendType: hierarchy,
|
||||||
|
|
@ -915,7 +1089,7 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
||||||
isFallback := false
|
isFallback := false
|
||||||
if len(recommends) == 0 {
|
if len(recommends) == 0 {
|
||||||
logger.Info(ctx, "fuzzy search without result", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
|
logger.Info(ctx, "fuzzy search without result", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
|
||||||
recommends, err = fallbackCurrentLevelByInput(ctx, rdb, hierarchy, inputSlice)
|
recommends, offset, err = fallbackCurrentLevelByInput(ctx, rdb, hierarchy, recommendLenType, inputSlice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
|
logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
|
||||||
return SearchResult{
|
return SearchResult{
|
||||||
|
|
@ -927,12 +1101,6 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isFallback = true
|
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{
|
return SearchResult{
|
||||||
|
|
@ -979,7 +1147,6 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
||||||
logger.Error(ctx, "query key by redis fuzzy search failed", "query_key", fuzzyInput, "error", err)
|
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)
|
return nil, 0, fmt.Errorf("redisearch suggest failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
// 如果没有结果,退一步(删除最后一个字符)并继续循环
|
// 如果没有结果,退一步(删除最后一个字符)并继续循环
|
||||||
// TODO 考虑使用其他方式代替 for 循环退一字符的查询方式
|
// TODO 考虑使用其他方式代替 for 循环退一字符的查询方式
|
||||||
|
|
@ -1041,7 +1208,6 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
||||||
recommends = append(recommends, recommend)
|
recommends = append(recommends, recommend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(recommends) == 0 {
|
if len(recommends) == 0 {
|
||||||
searchInput = trimLastRune(searchInput)
|
searchInput = trimLastRune(searchInput)
|
||||||
searchInputLen = len([]rune(searchInput))
|
searchInputLen = len([]rune(searchInput))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -217,6 +218,138 @@ func TestNormalizeRecommendResultsKeepsParentPrefixOffsetForSpecificFallback(t *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsTrimsConfigFuzzySuffixes(t *testing.T) {
|
||||||
|
input := "110kV_TV-testProject1.cable_22-testProject1110kV_TV.base_extend.c"
|
||||||
|
offset := len([]rune(input))
|
||||||
|
results := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{
|
||||||
|
input + "apacity",
|
||||||
|
input + "ategory",
|
||||||
|
input + "urrent",
|
||||||
|
input + "ode",
|
||||||
|
},
|
||||||
|
IsFuzzy: true,
|
||||||
|
Offset: offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeRecommendResults(input, results)
|
||||||
|
result := got[constants.MeasTagRecommendHierarchyType.String()]
|
||||||
|
want := []string{"apacity", "ategory", "urrent", "ode"}
|
||||||
|
if result.Offset != offset {
|
||||||
|
t.Fatalf("expected offset %d, got %d", offset, result.Offset)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result.QueryDatas, want) {
|
||||||
|
t.Fatalf("expected trimmed recommends %#v, got %#v", want, result.QueryDatas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsTrimsToken4FallbackCandidates(t *testing.T) {
|
||||||
|
nsPath := "110kV_TV-testProject1"
|
||||||
|
input := nsPath + ".x"
|
||||||
|
offset := recommendPrefixOffset([]string{nsPath})
|
||||||
|
results := map[string]SearchResult{
|
||||||
|
constants.CompTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.CompTagRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{
|
||||||
|
nsPath + ".cable_22-testProject1110kV_TV",
|
||||||
|
nsPath + ".cable_23-testProject1110kV_TV",
|
||||||
|
},
|
||||||
|
IsFuzzy: true,
|
||||||
|
IsFallback: true,
|
||||||
|
Offset: offset,
|
||||||
|
},
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{
|
||||||
|
nsPath + ".IA_rms_CTA-testProject1",
|
||||||
|
nsPath + ".IB_rms_CTA-testProject1",
|
||||||
|
},
|
||||||
|
IsFuzzy: true,
|
||||||
|
IsFallback: true,
|
||||||
|
Offset: offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeRecommendResults(input, results)
|
||||||
|
for key, result := range got {
|
||||||
|
if result.Offset != offset {
|
||||||
|
t.Fatalf("expected offset %d for %s, got %d", offset, key, result.Offset)
|
||||||
|
}
|
||||||
|
for _, recommend := range result.QueryDatas {
|
||||||
|
if strings.HasPrefix(recommend, nsPath+".") {
|
||||||
|
t.Fatalf("expected trimmed token4 fallback recommend for %s, got %s", key, recommend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got[constants.CompTagRecommendHierarchyType.String()].QueryDatas[0] != "cable_22-testProject1110kV_TV" {
|
||||||
|
t.Fatalf("expected token5 suffixes, got %v", got[constants.CompTagRecommendHierarchyType.String()].QueryDatas)
|
||||||
|
}
|
||||||
|
if got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas[0] != "IA_rms_CTA-testProject1" {
|
||||||
|
t.Fatalf("expected token7 suffixes, got %v", got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsKeepsFullAndLocalConfigMeasurementSuffixesConsistent(t *testing.T) {
|
||||||
|
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
|
||||||
|
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
|
||||||
|
members := []string{"capacity", "category", "current"}
|
||||||
|
|
||||||
|
fullResults := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
|
||||||
|
IsFuzzy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
localResults := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
|
||||||
|
IsFuzzy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
|
||||||
|
localGot := normalizeRecommendResults(localPrefix+".", localResults)
|
||||||
|
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
||||||
|
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
||||||
|
if !reflect.DeepEqual(fullRecommends, localRecommends) {
|
||||||
|
t.Fatalf("expected full/local config measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsKeepsFullAndLocalBayMeasurementSuffixesConsistent(t *testing.T) {
|
||||||
|
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
||||||
|
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
||||||
|
members := []string{"IA_rms", "IB_rms", "IC_rms"}
|
||||||
|
|
||||||
|
fullResults := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
|
||||||
|
IsFuzzy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
localResults := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
|
||||||
|
IsFuzzy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
|
||||||
|
localGot := normalizeRecommendResults(localPrefix+".", localResults)
|
||||||
|
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
||||||
|
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
||||||
|
if !reflect.DeepEqual(fullRecommends, localRecommends) {
|
||||||
|
t.Fatalf("expected full/local bay measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRecommendGroupTokens(t *testing.T) {
|
func TestRecommendGroupTokens(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -259,6 +392,242 @@ func TestRecommendGroupTokens(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigRecommendCompTag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputSlice []string
|
||||||
|
want string
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full token1 to token6 input",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
|
||||||
|
want: "comp_tag",
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local token4 to token6 input",
|
||||||
|
inputSlice: []string{"nspath", "comp_tag", "b"},
|
||||||
|
want: "comp_tag",
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing comp tag",
|
||||||
|
inputSlice: []string{"nspath"},
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, gotOK := configRecommendCompTag(tt.inputSlice)
|
||||||
|
if gotOK != tt.wantOK {
|
||||||
|
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("expected compTag %q, got %q", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterMembersByTrimmedPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
members []string
|
||||||
|
searchInput string
|
||||||
|
wantMembers []string
|
||||||
|
wantMatchInput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "config typo falls back to previous rune",
|
||||||
|
members: []string{"bay", "base_extend", "model", "rated", "stable", "component"},
|
||||||
|
searchInput: "bx",
|
||||||
|
wantMembers: []string{"bay", "base_extend"},
|
||||||
|
wantMatchInput: "b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "measurement typo falls back to previous rune",
|
||||||
|
members: []string{"I_A_rms", "I_B_rms", "U_A_rms"},
|
||||||
|
searchInput: "Ix",
|
||||||
|
wantMembers: []string{"I_A_rms", "I_B_rms"},
|
||||||
|
wantMatchInput: "I",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no fallback to empty prefix",
|
||||||
|
members: []string{"bay", "base_extend"},
|
||||||
|
searchInput: "x",
|
||||||
|
wantMembers: []string{},
|
||||||
|
wantMatchInput: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotMembers, gotMatchInput := filterMembersByTrimmedPrefix(tt.members, tt.searchInput)
|
||||||
|
if !reflect.DeepEqual(gotMembers, tt.wantMembers) {
|
||||||
|
t.Fatalf("expected members %#v, got %#v", tt.wantMembers, gotMembers)
|
||||||
|
}
|
||||||
|
if gotMatchInput != tt.wantMatchInput {
|
||||||
|
t.Fatalf("expected matched input %q, got %q", tt.wantMatchInput, gotMatchInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDotAndTypoInputsShareTrimmedPrefixFallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dotInput string
|
||||||
|
typoInput string
|
||||||
|
members []string
|
||||||
|
wantSuffix []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token1 grid",
|
||||||
|
dotInput: "g.",
|
||||||
|
typoInput: "gx",
|
||||||
|
members: []string{"grid000", "grid001", "zone000"},
|
||||||
|
wantSuffix: []string{"grid000", "grid001"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token2 zone",
|
||||||
|
dotInput: "z.",
|
||||||
|
typoInput: "zx",
|
||||||
|
members: []string{"zone000", "zone001", "station000"},
|
||||||
|
wantSuffix: []string{"zone000", "zone001"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token3 station",
|
||||||
|
dotInput: "s.",
|
||||||
|
typoInput: "sx",
|
||||||
|
members: []string{"station000", "station001", "zone000"},
|
||||||
|
wantSuffix: []string{"station000", "station001"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token4 nspath",
|
||||||
|
dotInput: "1.",
|
||||||
|
typoInput: "1x",
|
||||||
|
members: []string{"110kV_TV-demoProject", "110kV_TV-testProject1", "220kV_TV-demoProject"},
|
||||||
|
wantSuffix: []string{"110kV_TV-demoProject", "110kV_TV-testProject1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token5 component tag",
|
||||||
|
dotInput: "c.",
|
||||||
|
typoInput: "cx",
|
||||||
|
members: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV", "bay"},
|
||||||
|
wantSuffix: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token6 config",
|
||||||
|
dotInput: "b.",
|
||||||
|
typoInput: "bx",
|
||||||
|
members: []string{"bay", "base_extend", "component"},
|
||||||
|
wantSuffix: []string{"bay", "base_extend"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token7 measurement",
|
||||||
|
dotInput: "I.",
|
||||||
|
typoInput: "Ix",
|
||||||
|
members: []string{"IA_rms", "IB_rms", "UA_rms"},
|
||||||
|
wantSuffix: []string{"IA_rms", "IB_rms"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dotSearchInput := strings.TrimSuffix(tt.dotInput, ".")
|
||||||
|
typoSearchInput := trimLastRune(tt.typoInput)
|
||||||
|
dotMembers, dotMatchedInput := filterMembersByTrimmedPrefix(tt.members, dotSearchInput)
|
||||||
|
typoMembers, typoMatchedInput := filterMembersByTrimmedPrefix(tt.members, typoSearchInput)
|
||||||
|
|
||||||
|
if dotMatchedInput != typoMatchedInput {
|
||||||
|
t.Fatalf("expected dot and typo matched input to be equal, dot=%q typo=%q", dotMatchedInput, typoMatchedInput)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(dotMembers, typoMembers) {
|
||||||
|
t.Fatalf("expected dot and typo members to be equal, dot=%#v typo=%#v", dotMembers, typoMembers)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(dotMembers, tt.wantSuffix) {
|
||||||
|
t.Fatalf("expected members %#v, got %#v", tt.wantSuffix, dotMembers)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExactMatchChecksForInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputSlice []string
|
||||||
|
want []exactMatchCheck
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token1 can be grid or local nspath",
|
||||||
|
inputSlice: []string{"g"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: constants.RedisAllGridSetKey, member: "g"},
|
||||||
|
{setKey: constants.RedisAllCompNSPathSetKey, member: "g"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 can be zone comp tag or nspath meas",
|
||||||
|
inputSlice: []string{"grid000", "z"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"), member: "z"},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"), member: "z"},
|
||||||
|
{setKey: constants.RedisAllCompTagSetKey, member: "z"},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "grid000"), member: "z"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 token3 can be station or local config",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "s"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecZoneStationSetKey, "zone000"), member: "s"},
|
||||||
|
{setKey: constants.RedisAllConfigSetKey, member: "s"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 token3 token4 can be nspath or local meas",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "1"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, "station000"), member: "1"},
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "zone000"), member: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 through token5 component tag",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "c"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "nspath"), member: "c"},
|
||||||
|
{setKey: constants.RedisAllCompTagSetKey, member: "c"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 through token6 config",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: constants.RedisAllConfigSetKey, member: "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 through token7 measurement",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "bay", "I"},
|
||||||
|
want: []exactMatchCheck{
|
||||||
|
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), member: "I"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := exactMatchChecksForInput(tt.inputSlice)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Fatalf("expected checks %#v, got %#v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFallbackSpecificSetKey(t *testing.T) {
|
func TestFallbackSpecificSetKey(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue