fix: improve measurement recommend validation and offset handling
- use unified response codes for measurement recommend handler - reject recommend inputs with continuous dots as invalid params - fix exact and continuation match offsets for recommend results - preserve correct offsets for fallback recommend scenarios - use hash-based confirmation for measurement token7 recommendations - write component and bay token7 hash indexes for HEXISTS validation - add tests for input validation, hash keys, and recommend offset cases
This commit is contained in:
parent
180b0f7843
commit
6f78d8e341
|
|
@ -2,7 +2,8 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"modelRT/constants"
|
"modelRT/constants"
|
||||||
"modelRT/logger"
|
"modelRT/logger"
|
||||||
|
|
@ -49,9 +50,13 @@ func MeasurementRecommendHandler(c *gin.Context) {
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
|
logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
Code: http.StatusBadRequest,
|
return
|
||||||
Msg: err.Error(),
|
}
|
||||||
|
if err := validateMeasurementRecommendInput(request.Input); err != nil {
|
||||||
|
logger.Warn(ctx, "invalid measurement recommend input", "input", request.Input, "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{
|
||||||
|
"input": request.Input,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -67,12 +72,8 @@ func MeasurementRecommendHandler(c *gin.Context) {
|
||||||
if recommendResult.Err != nil {
|
if recommendResult.Err != nil {
|
||||||
err := recommendResult.Err
|
err := recommendResult.Err
|
||||||
logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
|
logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
renderRespFailure(c, constants.RespCodeServerError, err.Error(), map[string]any{
|
||||||
Code: http.StatusInternalServerError,
|
"input": request.Input,
|
||||||
Msg: err.Error(),
|
|
||||||
Payload: map[string]any{
|
|
||||||
"input": request.Input,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +92,7 @@ func MeasurementRecommendHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.SuccessResponse{
|
renderRespSuccess(c, constants.RespCodeSuccess, "success", payload)
|
||||||
Code: http.StatusOK,
|
|
||||||
Msg: "success",
|
|
||||||
Payload: payload,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
|
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
|
||||||
|
|
@ -119,3 +116,10 @@ func orderedRecommendResults(recommendResults map[string]model.SearchResult) []m
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateMeasurementRecommendInput(input string) error {
|
||||||
|
if strings.Contains(input, "..") {
|
||||||
|
return errors.New("input contains continuous dots")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestValidateMeasurementRecommendInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "continuous dots",
|
||||||
|
input: "G..",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single separator",
|
||||||
|
input: "G.zone",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing dot",
|
||||||
|
input: "G.",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateMeasurementRecommendInput(tt.input)
|
||||||
|
if tt.valid && err != nil {
|
||||||
|
t.Fatalf("expected valid input, got error %v", err)
|
||||||
|
}
|
||||||
|
if !tt.valid && err == nil {
|
||||||
|
t.Fatalf("expected invalid input")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ func StoreComponentColumnRecommend(ctx context.Context, compTagToFullPath map[st
|
||||||
for compTag, fullPath := range compTagToFullPath {
|
for compTag, fullPath := range compTagToFullPath {
|
||||||
specCompMeasKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag)
|
specCompMeasKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag)
|
||||||
pipe.SAdd(ctx, specCompMeasKey, componentColumnMembers...)
|
pipe.SAdd(ctx, specCompMeasKey, componentColumnMembers...)
|
||||||
|
pipe.HSet(ctx, componentColumnGroupKey(compTag), componentColumnHashFields(componentColumnNames)...)
|
||||||
|
|
||||||
isLocalFullPath := isLocalCompTagToFullPath[compTag]
|
isLocalFullPath := isLocalCompTagToFullPath[compTag]
|
||||||
fullConfigTerm := fmt.Sprintf("%s.%s", fullPath, constants.ComponentConfigKey)
|
fullConfigTerm := fmt.Sprintf("%s.%s", fullPath, constants.ComponentConfigKey)
|
||||||
|
|
@ -80,3 +81,11 @@ func stringSliceToAny(values []string) []any {
|
||||||
}
|
}
|
||||||
return members
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func componentColumnGroupKey(compTag string) string {
|
||||||
|
return recommendGroupKey(compTag, constants.ComponentConfigKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func componentColumnHashFields(columnNames []string) []any {
|
||||||
|
return recommendHashFields(columnNames)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComponentColumnGroupKey(t *testing.T) {
|
||||||
|
got := componentColumnGroupKey("cable_26-demoProject110kV_TV")
|
||||||
|
want := "cable_26-demoProject110kV_TV_component"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("expected key %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComponentColumnHashFields(t *testing.T) {
|
||||||
|
got := componentColumnHashFields([]string{"global_uuid", "nspath"})
|
||||||
|
want := []any{"global_uuid", true, "nspath", true}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("expected fields %#v, got %#v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -184,6 +184,9 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
|
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
|
||||||
|
if len(measTags) > 0 {
|
||||||
|
pipe.HSet(ctx, recommendGroupKey(compTag, "bay"), recommendHashFields(measTags)...)
|
||||||
|
}
|
||||||
ac.AddTerms(sug...)
|
ac.AddTerms(sug...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func recommendGroupKey(compTag string, configToken string) string {
|
||||||
|
return fmt.Sprintf("%s_%s", compTag, configToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendHashFields(values []string) []any {
|
||||||
|
fields := make([]any, 0, len(values)*2)
|
||||||
|
for _, value := range values {
|
||||||
|
fields = append(fields, value, true)
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ type SearchResult struct {
|
||||||
// input redis key, used to distinguish which goroutine the result belongs to
|
// input redis key, used to distinguish which goroutine the result belongs to
|
||||||
RecommendType constants.RecommendHierarchyType
|
RecommendType constants.RecommendHierarchyType
|
||||||
QueryDatas []string
|
QueryDatas []string
|
||||||
IsFuzzy bool
|
IsFuzzy bool // 代表本次搜索是否是模糊搜索, 例如输入 grid1.
|
||||||
IsFallback bool
|
IsFallback bool
|
||||||
Offset int
|
Offset int
|
||||||
Err error
|
Err error
|
||||||
|
|
@ -277,13 +278,16 @@ 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)
|
if hasExactCompletion(result.QueryDatas) {
|
||||||
|
result.Offset = len([]rune(input))
|
||||||
|
} else if hasLevelContinuation(result.QueryDatas) {
|
||||||
|
result.Offset = len([]rune(input))
|
||||||
|
results[key] = result
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
result.Offset = calcSearchResultOffset(input, result.QueryDatas)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.QueryDatas = trimRecommendTerms(result.QueryDatas, result.Offset)
|
result.QueryDatas = trimRecommendTerms(result.QueryDatas, result.Offset)
|
||||||
results[key] = result
|
results[key] = result
|
||||||
|
|
@ -291,6 +295,16 @@ func normalizeRecommendResults(input string, results map[string]SearchResult) ma
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasExactCompletion 判断推荐项是否表示当前输入已经完整命中, 无需再补充后缀。
|
||||||
|
func hasExactCompletion(recommends []string) bool {
|
||||||
|
return slices.Contains(recommends, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasLevelContinuation 判断推荐项是否表示当前层级已命中, 可继续补充分隔符进入下一层。
|
||||||
|
func hasLevelContinuation(recommends []string) bool {
|
||||||
|
return slices.Contains(recommends, ".")
|
||||||
|
}
|
||||||
|
|
||||||
// calcSearchResultOffset 计算 input 与推荐项的最短公共前缀长度, 用作裁剪位置。
|
// calcSearchResultOffset 计算 input 与推荐项的最短公共前缀长度, 用作裁剪位置。
|
||||||
func calcSearchResultOffset(input string, recommends []string) int {
|
func calcSearchResultOffset(input string, recommends []string) int {
|
||||||
var minOffset int
|
var minOffset int
|
||||||
|
|
@ -303,15 +317,13 @@ func calcSearchResultOffset(input string, recommends []string) int {
|
||||||
return minOffset
|
return minOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxRecommendLength 返回推荐项最大长度, fallback 结果用它表示无需裁剪已有输入。
|
// recommendPrefixOffset 返回推荐结果中已由输入确定的前缀长度, 包含末尾分隔符。
|
||||||
func maxRecommendLength(recommends []string) int {
|
func recommendPrefixOffset(prefixTokens []string) int {
|
||||||
var maxLen int
|
prefix := strings.Join(prefixTokens, ".")
|
||||||
for _, recommend := range recommends {
|
if prefix == "" {
|
||||||
if length := len([]rune(recommend)); length > maxLen {
|
return 0
|
||||||
maxLen = length
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return maxLen
|
return len([]rune(prefix)) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// trimRecommendTerms 按 offset 截掉已输入前缀, 并去掉空值和重复值。
|
// trimRecommendTerms 按 offset 截掉已输入前缀, 并去掉空值和重复值。
|
||||||
|
|
@ -552,6 +564,45 @@ func combineQueryResultByInput(hierarchy constants.RecommendHierarchyType, recom
|
||||||
return recommandResults
|
return recommandResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// confirmMeasRecommendResultsByHash 用 token5-token6 hash 确认 token7 是否属于当前配置组。
|
||||||
|
func confirmMeasRecommendResultsByHash(ctx context.Context, rdb *redis.Client, inputSlice []string, members []string, recommends []string) []string {
|
||||||
|
compTag, configToken, ok := recommendGroupTokens(inputSlice)
|
||||||
|
if !ok {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashKey := recommendGroupKey(compTag, configToken)
|
||||||
|
confirmedResults := make([]string, 0, len(recommends))
|
||||||
|
for index, recommend := range recommends {
|
||||||
|
if index >= len(members) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := rdb.HExists(ctx, hashKey, members[index]).Result()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "measurement recommend hash confirmation failed", "hash_key", hashKey, "field", members[index], "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
confirmedResults = append(confirmedResults, recommend)
|
||||||
|
}
|
||||||
|
return confirmedResults
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendGroupTokens(inputSlice []string) (string, string, bool) {
|
||||||
|
searchInputIndex := len(inputSlice) - 1
|
||||||
|
compTagIndex := searchInputIndex - 2
|
||||||
|
configTokenIndex := searchInputIndex - 1
|
||||||
|
if compTagIndex < 0 || configTokenIndex < 0 || configTokenIndex >= len(inputSlice) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
compTag := inputSlice[compTagIndex]
|
||||||
|
configToken := inputSlice[configTokenIndex]
|
||||||
|
return compTag, configToken, compTag != "" && configToken != ""
|
||||||
|
}
|
||||||
|
|
||||||
// getSpecificKeyByLength 根据层级和父级 token 生成对应的 specific set key。
|
// getSpecificKeyByLength 根据层级和父级 token 生成对应的 specific set key。
|
||||||
func getSpecificKeyByLength(hierarchy constants.RecommendHierarchyType, keyPrefix string) string {
|
func getSpecificKeyByLength(hierarchy constants.RecommendHierarchyType, keyPrefix string) string {
|
||||||
switch hierarchy {
|
switch hierarchy {
|
||||||
|
|
@ -648,11 +699,30 @@ func handleNSPathMeasSearch(ctx context.Context, rdb *redis.Client, inputSlice [
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isFallback := false
|
||||||
|
if len(recommends) == 0 {
|
||||||
|
queryKey := fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, nsPath)
|
||||||
|
members, err := rdb.SMembers(ctx, queryKey).Result()
|
||||||
|
if err != nil && err != redis.Nil {
|
||||||
|
logger.Error(ctx, "fallback measurement tags by nspath failed", "key", queryKey, "nspath", nsPath, "error", err)
|
||||||
|
return SearchResult{
|
||||||
|
RecommendType: hierarchy,
|
||||||
|
QueryDatas: nil,
|
||||||
|
IsFuzzy: true,
|
||||||
|
IsFallback: true,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recommends = combineNSPathMeasResults(nsPath, members)
|
||||||
|
offset = recommendPrefixOffset([]string{nsPath})
|
||||||
|
isFallback = true
|
||||||
|
}
|
||||||
|
|
||||||
return SearchResult{
|
return SearchResult{
|
||||||
RecommendType: hierarchy,
|
RecommendType: hierarchy,
|
||||||
QueryDatas: recommends,
|
QueryDatas: recommends,
|
||||||
IsFuzzy: true,
|
IsFuzzy: true,
|
||||||
|
IsFallback: isFallback,
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
Err: nil,
|
Err: nil,
|
||||||
}
|
}
|
||||||
|
|
@ -770,7 +840,10 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
||||||
}
|
}
|
||||||
|
|
||||||
recommandResults := combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, members)
|
recommandResults := combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, members)
|
||||||
if hierarchy == constants.ConfigRecommendHierarchyType || hierarchy == constants.MeasTagRecommendHierarchyType {
|
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
||||||
|
recommandResults = confirmMeasRecommendResultsByHash(ctx, rdb, inputSlice, members, recommandResults)
|
||||||
|
}
|
||||||
|
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
|
// 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
|
||||||
secondConfirmResults := make([]string, 0, len(recommandResults))
|
secondConfirmResults := make([]string, 0, len(recommandResults))
|
||||||
for _, res := range recommandResults {
|
for _, res := range recommandResults {
|
||||||
|
|
@ -854,7 +927,12 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isFallback = true
|
isFallback = true
|
||||||
offset = 0
|
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{
|
||||||
|
|
@ -871,17 +949,18 @@ 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
|
||||||
|
var memberComparePrefix string
|
||||||
searchInputLen := len([]rune(searchInput))
|
searchInputLen := len([]rune(searchInput))
|
||||||
compareHierarchyLen := int(hierarchy)
|
compareHierarchyLen := int(hierarchy)
|
||||||
comparePrefix = searchPrefix
|
comparePrefix = searchPrefix
|
||||||
|
memberComparePrefix = searchPrefix
|
||||||
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
||||||
compareHierarchyLen = int(hierarchy) - 1
|
|
||||||
lastDotIndex := strings.LastIndex(searchPrefix, ".")
|
lastDotIndex := strings.LastIndex(searchPrefix, ".")
|
||||||
if lastDotIndex == -1 {
|
if lastDotIndex == -1 {
|
||||||
configToken = ""
|
configToken = ""
|
||||||
} else {
|
} else {
|
||||||
configToken = searchPrefix[lastDotIndex+1:]
|
configToken = searchPrefix[lastDotIndex+1:]
|
||||||
comparePrefix = searchPrefix[:lastDotIndex]
|
memberComparePrefix = searchPrefix[:lastDotIndex]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -941,7 +1020,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
||||||
}
|
}
|
||||||
|
|
||||||
if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings.HasPrefix(termLastPart, searchInput) {
|
if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings.HasPrefix(termLastPart, searchInput) {
|
||||||
exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, comparePrefix, termLastPart)
|
exists, err := fuzzyRecommendMemberExists(ctx, rdb, hierarchy, memberComparePrefix, termLastPart)
|
||||||
if err != nil || !exists {
|
if err != nil || !exists {
|
||||||
logger.Info(ctx, "fuzzy recommend term not found in specific redis set",
|
logger.Info(ctx, "fuzzy recommend term not found in specific redis set",
|
||||||
"hierarchy", hierarchy,
|
"hierarchy", hierarchy,
|
||||||
|
|
@ -956,7 +1035,7 @@ func runFuzzySearch(ctx context.Context, rdb *redis.Client, searchInput string,
|
||||||
}
|
}
|
||||||
|
|
||||||
recommend := result.Term
|
recommend := result.Term
|
||||||
if hierarchy == constants.MeasTagRecommendHierarchyType {
|
if hierarchy == constants.MeasTagRecommendHierarchyType && configToken != "" && termPrefix == memberComparePrefix {
|
||||||
recommend = strings.Join([]string{termPrefix, configToken, termLastPart}, ".")
|
recommend = strings.Join([]string{termPrefix, configToken, termLastPart}, ".")
|
||||||
}
|
}
|
||||||
recommends = append(recommends, recommend)
|
recommends = append(recommends, recommend)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"modelRT/constants"
|
"modelRT/constants"
|
||||||
|
|
@ -44,6 +45,220 @@ func TestFuzzyRecommendOffsetUsesFullMatchedInput(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsUsesInputLengthForExactCompletion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full token1 to token7 structure",
|
||||||
|
input: "grid000.zone000.station000.220kV_学府路1-testProject1.compTag.config.IA_rms_CTA-testProject1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local token4 to token7 structure",
|
||||||
|
input: "220kV_学府路1-testProject1.IA_rms_CTA-testProject1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
results := map[string]SearchResult{
|
||||||
|
constants.MeasTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.MeasTagRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{""},
|
||||||
|
IsFuzzy: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeRecommendResults(tt.input, results)
|
||||||
|
result := got[constants.MeasTagRecommendHierarchyType.String()]
|
||||||
|
wantOffset := len([]rune(tt.input))
|
||||||
|
if result.Offset != wantOffset {
|
||||||
|
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
|
||||||
|
}
|
||||||
|
if len(result.QueryDatas) != 0 {
|
||||||
|
t.Fatalf("expected exact completion to return no suffix, got %v", result.QueryDatas)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsUsesInputLengthForLevelContinuation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
recommendType constants.RecommendHierarchyType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token1 grid can continue",
|
||||||
|
input: "grid000",
|
||||||
|
recommendType: constants.GridRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 zone can continue",
|
||||||
|
input: "grid000.zone000",
|
||||||
|
recommendType: constants.ZoneRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 token3 station can continue",
|
||||||
|
input: "grid000.zone000.station000",
|
||||||
|
recommendType: constants.StationRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 token3 token4 nspath can continue",
|
||||||
|
input: "grid000.zone000.station000.110kV_TV-demoProject",
|
||||||
|
recommendType: constants.CompNSPathRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 token2 token3 token4 token5 compTag can continue",
|
||||||
|
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV",
|
||||||
|
recommendType: constants.CompTagRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token1 through token6 config can continue",
|
||||||
|
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV.base_extend",
|
||||||
|
recommendType: constants.ConfigRecommendHierarchyType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeRecommendResults(tt.input, results)
|
||||||
|
result := got[tt.recommendType.String()]
|
||||||
|
wantOffset := len([]rune(tt.input))
|
||||||
|
if result.Offset != wantOffset {
|
||||||
|
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
|
||||||
|
}
|
||||||
|
if len(result.QueryDatas) != 1 || result.QueryDatas[0] != "." {
|
||||||
|
t.Fatalf("expected level continuation '.', got %v", result.QueryDatas)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsKeepsFallbackOffsetZero(t *testing.T) {
|
||||||
|
input := "x"
|
||||||
|
results := map[string]SearchResult{
|
||||||
|
constants.GridRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.GridRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{"grid000", "grid001"},
|
||||||
|
IsFuzzy: true,
|
||||||
|
IsFallback: true,
|
||||||
|
Offset: 0,
|
||||||
|
},
|
||||||
|
constants.CompNSPathRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.CompNSPathRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{"nspath000", "nspath001"},
|
||||||
|
IsFuzzy: true,
|
||||||
|
IsFallback: true,
|
||||||
|
Offset: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeRecommendResults(input, results)
|
||||||
|
for key, result := range got {
|
||||||
|
if result.Offset != 0 {
|
||||||
|
t.Fatalf("expected fallback offset 0 for %s, got %d", key, result.Offset)
|
||||||
|
}
|
||||||
|
if len(result.QueryDatas) != 2 {
|
||||||
|
t.Fatalf("expected fallback recommends to remain intact for %s, got %v", key, result.QueryDatas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRecommendResultsKeepsParentPrefixOffsetForSpecificFallback(t *testing.T) {
|
||||||
|
nsPath := "220kV_学府路1-testProject1"
|
||||||
|
input := nsPath + ".x"
|
||||||
|
offset := recommendPrefixOffset([]string{nsPath})
|
||||||
|
results := map[string]SearchResult{
|
||||||
|
constants.CompTagRecommendHierarchyType.String(): {
|
||||||
|
RecommendType: constants.CompTagRecommendHierarchyType,
|
||||||
|
QueryDatas: []string{
|
||||||
|
nsPath + ".CTA-testProject1220kV_学府路1",
|
||||||
|
nsPath + ".CB-testProject1220kV_学府路1",
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
if offset != 24 {
|
||||||
|
t.Fatalf("expected sample parent prefix offset 24, got %d", offset)
|
||||||
|
}
|
||||||
|
for key, result := range got {
|
||||||
|
if result.Offset != 24 {
|
||||||
|
t.Fatalf("expected specific fallback offset 24 for %s, got %d", key, result.Offset)
|
||||||
|
}
|
||||||
|
for _, recommend := range result.QueryDatas {
|
||||||
|
if strings.HasPrefix(recommend, nsPath+".") {
|
||||||
|
t.Fatalf("expected trimmed fallback recommend for %s, got %s", key, recommend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecommendGroupTokens(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputSlice []string
|
||||||
|
wantCompTag string
|
||||||
|
wantConfig string
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full token1 to token7 input",
|
||||||
|
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "base_extend", ""},
|
||||||
|
wantCompTag: "comp_tag",
|
||||||
|
wantConfig: "base_extend",
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local token4 to token7 input",
|
||||||
|
inputSlice: []string{"nspath", "comp_tag", "component", ""},
|
||||||
|
wantCompTag: "comp_tag",
|
||||||
|
wantConfig: "component",
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing config token",
|
||||||
|
inputSlice: []string{"nspath", ""},
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotCompTag, gotConfig, gotOK := recommendGroupTokens(tt.inputSlice)
|
||||||
|
if gotOK != tt.wantOK {
|
||||||
|
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
|
||||||
|
}
|
||||||
|
if gotCompTag != tt.wantCompTag || gotConfig != tt.wantConfig {
|
||||||
|
t.Fatalf("expected compTag/config %q/%q, got %q/%q", tt.wantCompTag, tt.wantConfig, gotCompTag, gotConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFallbackSpecificSetKey(t *testing.T) {
|
func TestFallbackSpecificSetKey(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue