fix: flatten measurement recommend response and normalize offsets

- return a single measurement recommend payload instead of grouped payloads
- remove recommended_type from response schema and regenerated swagger docs
- normalize Redis recommend results in model layer with stable type ordering
- deduplicate trimmed recommend terms and preserve the maximum response offset
This commit is contained in:
douxu 2026-06-29 14:13:58 +08:00
parent 8faba682b3
commit 367de31247
6 changed files with 118 additions and 70 deletions

View File

@ -524,10 +524,6 @@ const docTemplate = `{
" \"I_B_rms\"", " \"I_B_rms\"",
"\"I_C_rms\"]" "\"I_C_rms\"]"
] ]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
} }
} }
}, },

View File

@ -518,10 +518,6 @@
" \"I_B_rms\"", " \"I_B_rms\"",
"\"I_C_rms\"]" "\"I_C_rms\"]"
] ]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
} }
} }
}, },

View File

@ -86,9 +86,6 @@ definitions:
items: items:
type: string type: string
type: array type: array
recommended_type:
example: grid_tag
type: string
type: object type: object
network.RealTimeDataPayload: network.RealTimeDataPayload:
properties: properties:

View File

@ -2,10 +2,10 @@
package handler package handler
import ( import (
"modelRT/constants"
"modelRT/logger" "modelRT/logger"
"modelRT/model" "modelRT/model"
"modelRT/network" "modelRT/network"
"modelRT/util"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -55,8 +55,14 @@ func MeasurementRecommendHandler(c *gin.Context) {
} }
recommendResults := model.RedisSearchRecommend(c, request.Input) recommendResults := model.RedisSearchRecommend(c, request.Input)
payloads := make([]network.MeasurementRecommendPayload, 0, len(recommendResults)) payload := network.MeasurementRecommendPayload{
for _, recommendResult := range recommendResults { Input: request.Input,
RecommendedList: make([]string, 0),
}
seen := make(map[string]struct{})
orderedResults := orderedRecommendResults(recommendResults)
for _, recommendResult := range orderedResults {
if recommendResult.Err != nil { if recommendResult.Err != nil {
err := recommendResult.Err err := recommendResult.Err
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err) logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
@ -70,52 +76,45 @@ func MeasurementRecommendHandler(c *gin.Context) {
return return
} }
var finalOffset int if recommendResult.Offset > payload.Offset {
recommends := recommendResult.QueryDatas payload.Offset = recommendResult.Offset
if recommendResult.IsFuzzy {
var maxOffset int
for index, recommend := range recommends {
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset > maxOffset {
maxOffset = offset
}
}
finalOffset = maxOffset
} else {
var minOffset int
for index, recommend := range recommends {
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset < minOffset {
minOffset = offset
}
}
finalOffset = minOffset
}
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:]
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
}
} }
} }
payloads = append(payloads, network.MeasurementRecommendPayload{ for _, recommendResult := range orderedResults {
Input: request.Input, for _, recommend := range recommendResult.QueryDatas {
Offset: finalOffset, if _, exists := seen[recommend]; !exists {
RecommendType: recommendResult.RecommendType.String(), seen[recommend] = struct{}{}
RecommendedList: resultRecommends, payload.RecommendedList = append(payload.RecommendedList, recommend)
}) }
}
} }
c.JSON(http.StatusOK, network.SuccessResponse{ c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK, Code: http.StatusOK,
Msg: "success", Msg: "success",
Payload: &payloads, Payload: payload,
}) })
} }
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
orderedTypes := []string{
constants.CompNSPathRecommendHierarchyType.String(),
constants.GridRecommendHierarchyType.String(),
constants.ZoneRecommendHierarchyType.String(),
constants.StationRecommendHierarchyType.String(),
constants.CompTagRecommendHierarchyType.String(),
constants.ConfigRecommendHierarchyType.String(),
constants.MeasTagRecommendHierarchyType.String(),
}
results := make([]model.SearchResult, 0, len(recommendResults))
for _, recommendType := range orderedTypes {
result, ok := recommendResults[recommendType]
if !ok {
continue
}
results = append(results, result)
}
return results
}

View File

@ -24,6 +24,7 @@ type SearchResult struct {
RecommendType constants.RecommendHierarchyType RecommendType constants.RecommendHierarchyType
QueryDatas []string QueryDatas []string
IsFuzzy bool IsFuzzy bool
Offset int
Err error Err error
} }
@ -72,7 +73,7 @@ func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy const
} }
// process fuzzy search result // process fuzzy search result
recommends, err := runFuzzySearch(ctx, rdb, searchInput, "", hierarchy, recommendLenType) recommends, offset, err := runFuzzySearch(ctx, rdb, searchInput, "", hierarchy, recommendLenType)
if err != nil { if err != nil {
logger.Error(ctx, fmt.Sprintf("fuzzy search failed for %s hierarchical", util.GetLevelStrByRdsKey(searchRedisKey)), "search_input", searchInput, "error", err) logger.Error(ctx, fmt.Sprintf("fuzzy search failed for %s hierarchical", util.GetLevelStrByRdsKey(searchRedisKey)), "search_input", searchInput, "error", err)
fanInChan <- SearchResult{ fanInChan <- SearchResult{
@ -89,6 +90,7 @@ func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy const
RecommendType: hierarchy, RecommendType: hierarchy,
QueryDatas: recommends, QueryDatas: recommends,
IsFuzzy: true, IsFuzzy: true,
Offset: offset,
Err: nil, Err: nil,
} }
return return
@ -98,6 +100,7 @@ func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy const
RecommendType: hierarchy, RecommendType: hierarchy,
QueryDatas: []string{}, QueryDatas: []string{},
IsFuzzy: true, IsFuzzy: true,
Offset: offset,
Err: nil, Err: nil,
} }
} }
@ -105,6 +108,9 @@ func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy const
// RedisSearchRecommend define func of redis search by input string and return recommend results // RedisSearchRecommend define func of redis search by input string and return recommend results
func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchResult { func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchResult {
rdb := diagram.GetRedisClientInstance() rdb := diagram.GetRedisClientInstance()
finalizeResults := func(results map[string]SearchResult) map[string]SearchResult {
return normalizeRecommendResults(input, results)
}
if input == "" { if input == "" {
fanInChan := make(chan SearchResult, 2) fanInChan := make(chan SearchResult, 2)
@ -140,7 +146,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
} }
inputSlice := strings.Split(input, ".") inputSlice := strings.Split(input, ".")
@ -189,7 +195,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 2: case 2:
// zone tagname search // zone tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllZoneSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllZoneSetKey, inputSlice, fanInChan)
@ -205,7 +211,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 3: case 3:
// station tanname search // station tanname search
go handleLevelFuzzySearch(ctx, rdb, constants.StationRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllStationSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.StationRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllStationSetKey, inputSlice, fanInChan)
@ -222,7 +228,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 4: case 4:
// component nspath search // component nspath search
go handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompNSPathSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompNSPathSetKey, inputSlice, fanInChan)
@ -239,7 +245,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 5: case 5:
// component tagname search // component tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice, fanInChan)
@ -254,7 +260,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 6: case 6:
// config search // config search
go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice, fanInChan)
@ -269,7 +275,7 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(results)
case 7: case 7:
// measurement tagname search // measurement tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice, fanInChan) go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice, fanInChan)
@ -284,13 +290,61 @@ func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchRe
results[result.RecommendType.String()] = result results[result.RecommendType.String()] = result
} }
return results return finalizeResults(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
} }
} }
func normalizeRecommendResults(input string, results map[string]SearchResult) map[string]SearchResult {
for key, result := range results {
if result.Err != nil {
continue
}
if !result.IsFuzzy {
result.Offset = calcSearchResultOffset(input, result.QueryDatas)
}
result.QueryDatas = trimRecommendTerms(result.QueryDatas, result.Offset)
results[key] = result
}
return results
}
func calcSearchResultOffset(input string, recommends []string) int {
var minOffset int
for index, recommend := range recommends {
offset := util.GetLongestCommonPrefixLength(input, recommend)
if index == 0 || offset < minOffset {
minOffset = offset
}
}
return minOffset
}
func trimRecommendTerms(recommends []string, offset int) []string {
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
if offset > len(recommend) {
continue
}
recommendTerm := recommend[offset:]
if recommendTerm == "" {
continue
}
if _, exists := seen[recommendTerm]; exists {
continue
}
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
}
return resultRecommends
}
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()
@ -486,7 +540,6 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
} }
secondConfirmResults = append(secondConfirmResults, res) secondConfirmResults = append(secondConfirmResults, res)
} }
recommandResults = secondConfirmResults recommandResults = secondConfirmResults
} }
fanInChan <- SearchResult{ fanInChan <- SearchResult{
@ -528,7 +581,7 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
} }
// start redis fuzzy search // start redis fuzzy search
recommends, err := runFuzzySearch(ctx, rdb, searchInput, searchPrefix, hierarchy, recommendLenType) recommends, offset, err := runFuzzySearch(ctx, rdb, searchInput, searchPrefix, hierarchy, recommendLenType)
if err != nil { if err != nil {
logger.Error(ctx, "fuzzy search failed by hierarchy", "hierarchy", hierarchy, "search_input", searchInput, "error", err) logger.Error(ctx, "fuzzy search failed by hierarchy", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
fanInChan <- SearchResult{ fanInChan <- SearchResult{
@ -548,12 +601,13 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
RecommendType: hierarchy, RecommendType: hierarchy,
QueryDatas: recommends, QueryDatas: recommends,
IsFuzzy: true, IsFuzzy: true,
Offset: offset,
Err: nil, Err: nil,
} }
} }
// runFuzzySearch define func to process redis fuzzy search // runFuzzySearch define func to process redis fuzzy search
func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType, recommendLenType string) ([]string, 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(searchInput)
@ -583,7 +637,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
}) })
if err != nil { if err != nil {
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, 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 {
@ -618,13 +672,14 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
} }
} }
if recommendLenType == constants.FullRecommendLength { switch recommendLenType {
case constants.FullRecommendLength:
if result.Term == "" { if result.Term == "" {
termHierarchyLen = 1 termHierarchyLen = 1
} else { } else {
termHierarchyLen = strings.Count(result.Term, ".") + 1 termHierarchyLen = strings.Count(result.Term, ".") + 1
} }
} else if recommendLenType == constants.IsLocalRecommendLength { case constants.IsLocalRecommendLength:
if result.Term == "" { if result.Term == "" {
termHierarchyLen = 4 termHierarchyLen = 4
} else { } else {
@ -641,7 +696,13 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
} }
} }
return recommends, nil if len(recommends) == 0 {
searchInput = searchInput[:len(searchInput)-1]
searchInputLen = len(searchInput)
continue
} }
return []string{}, nil
return recommends, len(searchInput), nil
}
return []string{}, 0, nil
} }

View File

@ -26,7 +26,6 @@ type WSResponse struct {
type MeasurementRecommendPayload struct { type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."` Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"` Offset int `json:"offset" example:"21"`
RecommendType string `json:"recommended_type" example:"grid_tag"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"` RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
} }