fix: improve measurement recommend fallback and rune offsets

- fallback to all current-level candidates when fuzzy search has no result
- track fallback results and calculate fallback offsets from max rune length
- use rune-based prefix offsets and trimming for non-ASCII tokens
- reuse local nspath filtering for component nspath recommendations
This commit is contained in:
douxu 2026-06-30 13:55:42 +08:00
parent 367de31247
commit 66870c7008
2 changed files with 130 additions and 49 deletions

View File

@ -24,6 +24,7 @@ type SearchResult struct {
RecommendType constants.RecommendHierarchyType RecommendType constants.RecommendHierarchyType
QueryDatas []string QueryDatas []string
IsFuzzy bool IsFuzzy bool
IsFallback bool
Offset int Offset int
Err error Err error
} }
@ -96,11 +97,25 @@ func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy const
return return
} }
recommends, err = fallbackAllCurrentLevel(ctx, rdb, hierarchy)
if err != nil {
logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: true,
IsFallback: true,
Err: err,
}
return
}
fanInChan <- SearchResult{ fanInChan <- SearchResult{
RecommendType: hierarchy, RecommendType: hierarchy,
QueryDatas: []string{}, QueryDatas: recommends,
IsFuzzy: true, IsFuzzy: true,
Offset: offset, IsFallback: true,
Offset: 0,
Err: nil, Err: nil,
} }
} }
@ -127,21 +142,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
} }
if result.RecommendType == constants.CompNSPathRecommendHierarchyType { if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
filterResults := make([]string, 0, len(result.QueryDatas)) result.QueryDatas = filterLocalNSPathResults(result.QueryDatas)
// TODO 增加 nspath 过滤
for _, queryData := range result.QueryDatas {
var nsPath string
if lastDotIndex := strings.LastIndex(queryData, "."); lastDotIndex == -1 {
nsPath = queryData
} else {
nsPath = queryData[lastDotIndex+1:]
}
if isLocal, ok := NSPathToIsLocalMap[nsPath]; ok && isLocal {
filterResults = append(filterResults, queryData)
}
}
result.QueryDatas = filterResults
} }
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
@ -171,26 +172,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
} }
if result.RecommendType == constants.CompNSPathRecommendHierarchyType { if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
filterResults := make([]string, 0, len(result.QueryDatas)) result.QueryDatas = filterLocalNSPathResults(result.QueryDatas)
// TODO 增加 nspath 过滤
for _, queryData := range result.QueryDatas {
if queryData == "." {
filterResults = append(filterResults, queryData)
continue
}
var nsPath string
if lastDotIndex := strings.LastIndex(queryData, "."); lastDotIndex == -1 {
nsPath = queryData
} else {
nsPath = queryData[lastDotIndex+1:]
}
if isLocal, ok := NSPathToIsLocalMap[nsPath]; ok && isLocal {
filterResults = append(filterResults, queryData)
}
}
result.QueryDatas = filterResults
} }
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
@ -302,6 +284,11 @@ func normalizeRecommendResults(input string, results map[string]SearchResult) ma
if result.Err != nil { if result.Err != nil {
continue continue
} }
if result.IsFallback {
result.Offset = maxRecommendLength(result.QueryDatas)
results[key] = result
continue
}
if !result.IsFuzzy { if !result.IsFuzzy {
result.Offset = calcSearchResultOffset(input, result.QueryDatas) result.Offset = calcSearchResultOffset(input, result.QueryDatas)
} }
@ -322,16 +309,27 @@ func calcSearchResultOffset(input string, recommends []string) int {
return minOffset return minOffset
} }
func maxRecommendLength(recommends []string) int {
var maxLen int
for _, recommend := range recommends {
if length := len([]rune(recommend)); length > maxLen {
maxLen = length
}
}
return maxLen
}
func trimRecommendTerms(recommends []string, offset int) []string { func trimRecommendTerms(recommends []string, offset int) []string {
resultRecommends := make([]string, 0, len(recommends)) resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, recommend := range recommends { for _, recommend := range recommends {
if offset > len(recommend) { recommendRunes := []rune(recommend)
if offset > len(recommendRunes) {
continue continue
} }
recommendTerm := recommend[offset:] recommendTerm := string(recommendRunes[offset:])
if recommendTerm == "" { if recommendTerm == "" {
continue continue
} }
@ -345,11 +343,68 @@ func trimRecommendTerms(recommends []string, offset int) []string {
return resultRecommends return resultRecommends
} }
func filterLocalNSPathResults(queryDatas []string) []string {
filterResults := make([]string, 0, len(queryDatas))
for _, queryData := range queryDatas {
if queryData == "." {
filterResults = append(filterResults, queryData)
continue
}
var nsPath string
if lastDotIndex := strings.LastIndex(queryData, "."); lastDotIndex == -1 {
nsPath = queryData
} else {
nsPath = queryData[lastDotIndex+1:]
}
// TODO 考虑为 NSPathToIsLocalMap 增加读写锁或改为并发安全快照, 保证过滤数据一致性
if isLocal, ok := NSPathToIsLocalMap[nsPath]; ok && isLocal {
filterResults = append(filterResults, queryData)
}
}
return filterResults
}
func queryMemberFromSpecificsLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, keyPrefix string) ([]string, error) { func queryMemberFromSpecificsLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, keyPrefix string) ([]string, error) {
queryKey := getSpecificKeyByLength(hierarchy, keyPrefix) queryKey := getSpecificKeyByLength(hierarchy, keyPrefix)
return rdb.SMembers(ctx, queryKey).Result() return rdb.SMembers(ctx, queryKey).Result()
} }
func fallbackAllCurrentLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType) ([]string, error) {
setKey, ok := getAllSetKeyByHierarchy(hierarchy)
if !ok {
return nil, fmt.Errorf("unsupported recommend hierarchy for fallback: %s", hierarchy)
}
members, err := rdb.SMembers(ctx, setKey).Result()
if err != nil && err != redis.Nil {
return nil, err
}
return members, nil
}
func getAllSetKeyByHierarchy(hierarchy constants.RecommendHierarchyType) (string, bool) {
switch hierarchy {
case constants.GridRecommendHierarchyType:
return constants.RedisAllGridSetKey, true
case constants.ZoneRecommendHierarchyType:
return constants.RedisAllZoneSetKey, true
case constants.StationRecommendHierarchyType:
return constants.RedisAllStationSetKey, true
case constants.CompNSPathRecommendHierarchyType:
return constants.RedisAllCompNSPathSetKey, true
case constants.CompTagRecommendHierarchyType:
return constants.RedisAllCompTagSetKey, true
case constants.ConfigRecommendHierarchyType:
return constants.RedisAllConfigSetKey, true
case constants.MeasTagRecommendHierarchyType:
return constants.RedisAllMeasTagSetKey, true
default:
return "", false
}
}
func getAllKeyByGridLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) { func getAllKeyByGridLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) {
setKey := constants.RedisAllGridSetKey setKey := constants.RedisAllGridSetKey
hierarchy := constants.GridRecommendHierarchyType hierarchy := constants.GridRecommendHierarchyType
@ -593,14 +648,30 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
return return
} }
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 = fallbackAllCurrentLevel(ctx, rdb, hierarchy)
if err != nil {
logger.Error(ctx, "fallback all keys at current level failed", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: true,
IsFallback: true,
Err: err,
}
return
}
isFallback = true
offset = 0
} }
fanInChan <- SearchResult{ fanInChan <- SearchResult{
RecommendType: hierarchy, RecommendType: hierarchy,
QueryDatas: recommends, QueryDatas: recommends,
IsFuzzy: true, IsFuzzy: true,
IsFallback: isFallback,
Offset: offset, Offset: offset,
Err: nil, Err: nil,
} }
@ -610,7 +681,7 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
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
var comparePrefix string var comparePrefix string
searchInputLen := len(searchInput) searchInputLen := len([]rune(searchInput))
compareHierarchyLen := int(hierarchy) compareHierarchyLen := int(hierarchy)
comparePrefix = searchPrefix comparePrefix = searchPrefix
if hierarchy == constants.MeasTagRecommendHierarchyType { if hierarchy == constants.MeasTagRecommendHierarchyType {
@ -641,10 +712,10 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
} }
if len(results) == 0 { if len(results) == 0 {
// 如果没有结果,退一步(删除最后一个字)并继续循环 // 如果没有结果,退一步(删除最后一个字)并继续循环
// TODO 考虑使用其他方式代替 for 循环退一字的查询方式 // TODO 考虑使用其他方式代替 for 循环退一字的查询方式
searchInput = searchInput[:len(searchInput)-1] searchInput = trimLastRune(searchInput)
searchInputLen = len(searchInput) searchInputLen = len([]rune(searchInput))
continue continue
} }
@ -697,12 +768,20 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
} }
if len(recommends) == 0 { if len(recommends) == 0 {
searchInput = searchInput[:len(searchInput)-1] searchInput = trimLastRune(searchInput)
searchInputLen = len(searchInput) searchInputLen = len([]rune(searchInput))
continue continue
} }
return recommends, len(searchInput), nil return recommends, len([]rune(searchInput)), nil
} }
return []string{}, 0, nil return []string{}, 0, nil
} }
func trimLastRune(s string) string {
runes := []rune(s)
if len(runes) == 0 {
return s
}
return string(runes[:len(runes)-1])
}

View File

@ -46,10 +46,12 @@ func GetLongestCommonPrefixLength(query string, result string) int {
return 0 return 0
} }
minLen := min(len(query), len(result)) queryRunes := []rune(query)
resultRunes := []rune(result)
minLen := min(len(queryRunes), len(resultRunes))
for i := range minLen { for i := range minLen {
if query[i] != result[i] { if queryRunes[i] != resultRunes[i] {
return i return i
} }
} }