fix: improve redis recommendation fuzzy fallback handling
- Preserve level continuation marker when trimming recommendation terms - Add group fallback for config and measurement typo inputs - Validate fuzzy exact continuation with redis set membership - Use local nspath set for local fuzzy member checks - Avoid returning continuation marker for exact measurement tag matches - Add tests for fuzzy continuation and mismatch fallback behavior
This commit is contained in:
parent
578f805b57
commit
a5e8c2ba4e
|
|
@ -418,6 +418,15 @@ func trimRecommendTerms(recommends []string, offset int) []string {
|
|||
seen := make(map[string]struct{})
|
||||
|
||||
for _, recommend := range recommends {
|
||||
if recommend == "." {
|
||||
if _, exists := seen[recommend]; exists {
|
||||
continue
|
||||
}
|
||||
seen[recommend] = struct{}{}
|
||||
resultRecommends = append(resultRecommends, recommend)
|
||||
continue
|
||||
}
|
||||
|
||||
recommendRunes := []rune(recommend)
|
||||
if offset > len(recommendRunes) {
|
||||
continue
|
||||
|
|
@ -530,7 +539,7 @@ func fallbackConfigByInput(ctx context.Context, rdb *redis.Client, allSetKey str
|
|||
searchInput := inputSlice[len(inputSlice)-1]
|
||||
matchedMembers, matchedInput := filterMembersByTrimmedPrefix(members, searchInput)
|
||||
if len(matchedMembers) == 0 {
|
||||
return []string{}, 0, nil
|
||||
return fallbackConfigGroupByInput(ctx, rdb, recommendLenType, inputSlice, members)
|
||||
}
|
||||
|
||||
prefixTokens := inputSlice[:len(inputSlice)-1]
|
||||
|
|
@ -542,6 +551,16 @@ func fallbackConfigByInput(ctx context.Context, rdb *redis.Client, allSetKey str
|
|||
return recommends, fuzzyRecommendOffset(strings.Join(prefixTokens, "."), matchedInput), nil
|
||||
}
|
||||
|
||||
func fallbackConfigGroupByInput(ctx context.Context, rdb *redis.Client, recommendLenType string, inputSlice []string, members []string) ([]string, int, error) {
|
||||
recommends := combineQueryResultByInput(constants.ConfigRecommendHierarchyType, recommendLenType, inputSlice, members)
|
||||
recommends = confirmConfigRecommendResultsByHash(ctx, rdb, inputSlice, members, recommends)
|
||||
if len(recommends) == 0 {
|
||||
return []string{}, 0, nil
|
||||
}
|
||||
|
||||
return recommends, recommendPrefixOffset(inputSlice[:len(inputSlice)-1]), 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 {
|
||||
|
|
@ -551,7 +570,7 @@ func fallbackMeasByInput(ctx context.Context, rdb *redis.Client, allSetKey strin
|
|||
searchInput := inputSlice[len(inputSlice)-1]
|
||||
matchedMembers, matchedInput := filterMembersByTrimmedPrefix(members, searchInput)
|
||||
if len(matchedMembers) == 0 {
|
||||
return []string{}, 0, nil
|
||||
return fallbackMeasGroupByInput(ctx, rdb, recommendLenType, inputSlice)
|
||||
}
|
||||
|
||||
recommends := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, recommendLenType, inputSlice, matchedMembers)
|
||||
|
|
@ -563,6 +582,27 @@ func fallbackMeasByInput(ctx context.Context, rdb *redis.Client, allSetKey strin
|
|||
return recommends, fuzzyRecommendOffset(strings.Join(inputSlice[:len(inputSlice)-1], "."), matchedInput), nil
|
||||
}
|
||||
|
||||
func fallbackMeasGroupByInput(ctx context.Context, rdb *redis.Client, recommendLenType string, inputSlice []string) ([]string, int, error) {
|
||||
compTag, _, ok := recommendGroupTokens(inputSlice)
|
||||
if !ok {
|
||||
return []string{}, 0, nil
|
||||
}
|
||||
|
||||
queryKey := getSpecificKeyByLength(constants.MeasTagRecommendHierarchyType, compTag)
|
||||
members, err := rdb.SMembers(ctx, queryKey).Result()
|
||||
if err != nil && err != redis.Nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
recommends := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, recommendLenType, inputSlice, members)
|
||||
recommends = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, members, recommends)
|
||||
if len(recommends) == 0 {
|
||||
return []string{}, 0, nil
|
||||
}
|
||||
|
||||
return recommends, recommendPrefixOffset(inputSlice[:len(inputSlice)-1]), nil
|
||||
}
|
||||
|
||||
func filterMembersByTrimmedPrefix(members []string, searchInput string) ([]string, string) {
|
||||
for len([]rune(searchInput)) != 0 {
|
||||
filtered := make([]string, 0, len(members))
|
||||
|
|
@ -1060,7 +1100,7 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
|||
|
||||
if keyExists {
|
||||
var QueryData []string
|
||||
if hierarchy == constants.MaxIdentifyHierarchy || (hierarchy == constants.IdentifyHierarchy && redisSetKey == constants.RedisAllMeasTagSetKey) {
|
||||
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
||||
QueryData = []string{""}
|
||||
} else {
|
||||
QueryData = []string{"."}
|
||||
|
|
@ -1148,6 +1188,10 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
|||
return nil, 0, fmt.Errorf("redisearch suggest failed: %w", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
if fuzzyExactContinuationExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, searchInput, fuzzyInput) {
|
||||
return []string{"."}, fuzzyRecommendOffset(searchPrefix, searchInput), nil
|
||||
}
|
||||
|
||||
// 如果没有结果,退一步(删除最后一个字符)并继续循环
|
||||
// TODO 考虑使用其他方式代替 for 循环退一字符的查询方式
|
||||
searchInput = trimLastRune(searchInput)
|
||||
|
|
@ -1187,7 +1231,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
|||
}
|
||||
|
||||
if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings.HasPrefix(termLastPart, searchInput) {
|
||||
exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, memberComparePrefix, termLastPart)
|
||||
exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, termLastPart)
|
||||
if err != nil || !exists {
|
||||
logger.Info(ctx, "fuzzy recommend term not found in specific redis set",
|
||||
"hierarchy", hierarchy,
|
||||
|
|
@ -1201,6 +1245,11 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
|||
continue
|
||||
}
|
||||
|
||||
if termLastPart == searchInput {
|
||||
recommends = append(recommends, fuzzyExactContinuationValue(hierarchy))
|
||||
continue
|
||||
}
|
||||
|
||||
recommend := result.Term
|
||||
if hierarchy == constants.MeasTagRecommendHierarchyType && configToken != "" && termPrefix == memberComparePrefix {
|
||||
recommend = strings.Join([]string{termPrefix, configToken, termLastPart}, ".")
|
||||
|
|
@ -1209,16 +1258,42 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
|||
}
|
||||
}
|
||||
if len(recommends) == 0 {
|
||||
if fuzzyExactContinuationExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, searchInput, fuzzyInput) {
|
||||
return []string{fuzzyExactContinuationValue(hierarchy)}, fuzzyRecommendOffset(searchPrefix, searchInput), nil
|
||||
}
|
||||
|
||||
searchInput = trimLastRune(searchInput)
|
||||
searchInputLen = len([]rune(searchInput))
|
||||
continue
|
||||
}
|
||||
|
||||
return recommends, fuzzyRecommendOffset(searchPrefix, searchInput), nil
|
||||
offset := fuzzyRecommendOffset(searchPrefix, searchInput)
|
||||
return recommends, offset, nil
|
||||
}
|
||||
return []string{}, 0, nil
|
||||
}
|
||||
|
||||
func fuzzyExactContinuationExists(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, memberComparePrefix string, searchInput string, fuzzyInput string) bool {
|
||||
exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, recommendLenType, memberComparePrefix, searchInput)
|
||||
if err != nil {
|
||||
logger.Info(ctx, "fuzzy exact continuation check failed",
|
||||
"hierarchy", hierarchy,
|
||||
"recommend_len_type", recommendLenType,
|
||||
"query_key", fuzzyInput,
|
||||
"search_input", searchInput,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
func fuzzyExactContinuationValue(hierarchy constants.RecommendHierarchyType) string {
|
||||
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
||||
return ""
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
// fuzzyRecommendOffset 返回模糊命中的裁剪位置, 保留完整输入前缀参与 offset。
|
||||
func fuzzyRecommendOffset(searchPrefix string, searchInput string) int {
|
||||
if searchPrefix == "" {
|
||||
|
|
@ -1228,8 +1303,8 @@ func fuzzyRecommendOffset(searchPrefix string, searchInput string) int {
|
|||
}
|
||||
|
||||
// fuzzyRecommendMemberExists 校验模糊候选末级 token 是否存在于对应 Redis set。
|
||||
func fuzzyRecommendMemberExists(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, comparePrefix string, termLastPart string) (bool, error) {
|
||||
setKey, ok := fuzzyRecommendMemberSetKey(hierarchy, comparePrefix)
|
||||
func fuzzyRecommendMemberExists(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, comparePrefix string, termLastPart string) (bool, error) {
|
||||
setKey, ok := fuzzyRecommendMemberSetKey(hierarchy, recommendLenType, comparePrefix)
|
||||
if !ok {
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -1237,7 +1312,7 @@ func fuzzyRecommendMemberExists(ctx context.Context, rdb *redis.Client, hierarch
|
|||
}
|
||||
|
||||
// fuzzyRecommendMemberSetKey 返回模糊候选校验时应使用的 Redis set key。
|
||||
func fuzzyRecommendMemberSetKey(hierarchy constants.RecommendHierarchyType, comparePrefix string) (string, bool) {
|
||||
func fuzzyRecommendMemberSetKey(hierarchy constants.RecommendHierarchyType, recommendLenType string, comparePrefix string) (string, bool) {
|
||||
switch hierarchy {
|
||||
case constants.GridRecommendHierarchyType:
|
||||
return constants.RedisAllGridSetKey, true
|
||||
|
|
@ -1246,6 +1321,9 @@ func fuzzyRecommendMemberSetKey(hierarchy constants.RecommendHierarchyType, comp
|
|||
case constants.StationRecommendHierarchyType:
|
||||
return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != ""
|
||||
case constants.CompNSPathRecommendHierarchyType:
|
||||
if recommendLenType == constants.IsLocalRecommendLength {
|
||||
return constants.RedisAllCompNSPathSetKey, true
|
||||
}
|
||||
return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != ""
|
||||
case constants.CompTagRecommendHierarchyType:
|
||||
return fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, lastRecommendPrefixToken(comparePrefix)), comparePrefix != ""
|
||||
|
|
|
|||
|
|
@ -46,6 +46,78 @@ func TestFuzzyRecommendOffsetUsesFullMatchedInput(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFuzzyRecommendMemberSetKeyUsesLocalNSPathSet(t *testing.T) {
|
||||
setKey, ok := fuzzyRecommendMemberSetKey(constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, "")
|
||||
if !ok {
|
||||
t.Fatalf("expected local nspath fuzzy member check to use a redis set")
|
||||
}
|
||||
if setKey != constants.RedisAllCompNSPathSetKey {
|
||||
t.Fatalf("expected set key %q, got %q", constants.RedisAllCompNSPathSetKey, setKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRecommendResultsKeepsFuzzyExactContinuation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
recommendType constants.RecommendHierarchyType
|
||||
offset int
|
||||
}{
|
||||
{
|
||||
name: "local nspath typo completes current level",
|
||||
input: "110kV_TV-demoProjectx",
|
||||
recommendType: constants.CompNSPathRecommendHierarchyType,
|
||||
offset: len([]rune("110kV_TV-demoProject")),
|
||||
},
|
||||
{
|
||||
name: "grid typo completes current level",
|
||||
input: "grid000x",
|
||||
recommendType: constants.GridRecommendHierarchyType,
|
||||
offset: len([]rune("grid000")),
|
||||
},
|
||||
{
|
||||
name: "zone typo completes current level",
|
||||
input: "grid000.zone000x",
|
||||
recommendType: constants.ZoneRecommendHierarchyType,
|
||||
offset: len([]rune("grid000.zone000")),
|
||||
},
|
||||
{
|
||||
name: "station typo completes current level",
|
||||
input: "grid000.zone000.station000x",
|
||||
recommendType: constants.StationRecommendHierarchyType,
|
||||
offset: len([]rune("grid000.zone000.station000")),
|
||||
},
|
||||
{
|
||||
name: "full nspath typo completes current level",
|
||||
input: "grid000.zone000.station000.110kV_TV-demoProjectx",
|
||||
recommendType: constants.CompNSPathRecommendHierarchyType,
|
||||
offset: len([]rune("grid000.zone000.station000.110kV_TV-demoProject")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
results := map[string]SearchResult{
|
||||
tt.recommendType.String(): {
|
||||
RecommendType: tt.recommendType,
|
||||
QueryDatas: []string{"."},
|
||||
IsFuzzy: true,
|
||||
Offset: tt.offset,
|
||||
},
|
||||
}
|
||||
|
||||
got := normalizeRecommendResults(tt.input, results)
|
||||
result := got[tt.recommendType.String()]
|
||||
if result.Offset != tt.offset {
|
||||
t.Fatalf("expected offset %d, got %d", tt.offset, result.Offset)
|
||||
}
|
||||
if !reflect.DeepEqual(result.QueryDatas, []string{"."}) {
|
||||
t.Fatalf("expected fuzzy exact continuation '.', got %#v", result.QueryDatas)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRecommendResultsUsesInputLengthForExactCompletion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -350,6 +422,78 @@ func TestNormalizeRecommendResultsKeepsFullAndLocalBayMeasurementSuffixesConsist
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRecommendResultsKeepsConfigMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
|
||||
prefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
||||
members := []string{"bay", "base_extend", "model", "component"}
|
||||
groupResults := combineQueryResultByInput(constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(prefix+".", "."), members)
|
||||
offset := recommendPrefixOffset(strings.Split(prefix, "."))
|
||||
|
||||
emptyInputResults := map[string]SearchResult{
|
||||
constants.ConfigRecommendHierarchyType.String(): {
|
||||
RecommendType: constants.ConfigRecommendHierarchyType,
|
||||
QueryDatas: append([]string{}, groupResults...),
|
||||
IsFuzzy: false,
|
||||
},
|
||||
}
|
||||
mismatchFallbackResults := map[string]SearchResult{
|
||||
constants.ConfigRecommendHierarchyType.String(): {
|
||||
RecommendType: constants.ConfigRecommendHierarchyType,
|
||||
QueryDatas: append([]string{}, groupResults...),
|
||||
IsFuzzy: true,
|
||||
IsFallback: true,
|
||||
Offset: offset,
|
||||
},
|
||||
}
|
||||
|
||||
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
|
||||
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
|
||||
emptyResult := emptyGot[constants.ConfigRecommendHierarchyType.String()]
|
||||
mismatchResult := mismatchGot[constants.ConfigRecommendHierarchyType.String()]
|
||||
|
||||
if mismatchResult.Offset != emptyResult.Offset {
|
||||
t.Fatalf("expected config mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
|
||||
}
|
||||
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
|
||||
t.Fatalf("expected config mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRecommendResultsKeepsMeasurementMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
|
||||
prefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
||||
members := []string{"IA_rms_CTA-testProject1", "IB_rms_CTA-testProject1", "IC_rms_CTA-testProject1"}
|
||||
groupResults := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(prefix+".", "."), members)
|
||||
offset := recommendPrefixOffset(strings.Split(prefix, "."))
|
||||
|
||||
emptyInputResults := map[string]SearchResult{
|
||||
constants.MeasTagRecommendHierarchyType.String(): {
|
||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||
QueryDatas: append([]string{}, groupResults...),
|
||||
IsFuzzy: false,
|
||||
},
|
||||
}
|
||||
mismatchFallbackResults := map[string]SearchResult{
|
||||
constants.MeasTagRecommendHierarchyType.String(): {
|
||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||
QueryDatas: append([]string{}, groupResults...),
|
||||
IsFuzzy: true,
|
||||
IsFallback: true,
|
||||
Offset: offset,
|
||||
},
|
||||
}
|
||||
|
||||
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
|
||||
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
|
||||
emptyResult := emptyGot[constants.MeasTagRecommendHierarchyType.String()]
|
||||
mismatchResult := mismatchGot[constants.MeasTagRecommendHierarchyType.String()]
|
||||
|
||||
if mismatchResult.Offset != emptyResult.Offset {
|
||||
t.Fatalf("expected measurement mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
|
||||
}
|
||||
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
|
||||
t.Fatalf("expected measurement mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendGroupTokens(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Reference in New Issue