modelRT/model/redis_recommend.go

649 lines
21 KiB
Go

// Package model define model struct of model runtime service
package model
import (
"context"
"errors"
"fmt"
"math"
"strings"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/util"
"github.com/RediSearch/redisearch-go/v2/redisearch"
redigo "github.com/gomodule/redigo/redis"
"github.com/redis/go-redis/v9"
)
// SearchResult define struct to store redis query recommend search result
type SearchResult struct {
// input redis key, used to distinguish which goroutine the result belongs to
RecommendType constants.RecommendHierarchyType
QueryDatas []string
IsFuzzy bool
Err error
}
var ac *redisearch.Autocompleter
// InitAutocompleterWithPool define func of initialize the Autocompleter with redigo pool
func InitAutocompleterWithPool(pool *redigo.Pool) {
ac = redisearch.NewAutocompleterFromPool(pool, constants.RedisSearchDictName)
}
func levelOneRedisSearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, searchInput string, searchRedisKey string, fanInChan chan SearchResult) {
defer func() {
if r := recover(); r != nil {
logger.Error(ctx, "searchFunc panicked", "panic", r)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: errors.New("search goroutine panicked"),
}
}
}()
exists, err := rdb.SIsMember(ctx, searchRedisKey, searchInput).Result()
if err != nil {
logger.Error(ctx, "redis membership check failed", "key", searchRedisKey, "member", searchInput, "op", "SIsMember", "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
// the input key is the complete hierarchical value
if exists {
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: []string{"."},
IsFuzzy: false,
Err: nil,
}
return
}
// process fuzzy search result
recommends, err := runFuzzySearch(ctx, rdb, searchInput, "", hierarchy, recommendLenType)
if err != nil {
logger.Error(ctx, fmt.Sprintf("fuzzy search failed for %s hierarchical", util.GetLevelStrByRdsKey(searchRedisKey)), "search_input", searchInput, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
if len(recommends) > 0 {
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: recommends,
IsFuzzy: true,
Err: nil,
}
return
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: []string{},
IsFuzzy: true,
Err: nil,
}
}
// RedisSearchRecommend define func of redis search by input string and return recommend results
func RedisSearchRecommend(ctx context.Context, input string) map[string]SearchResult {
rdb := diagram.GetRedisClientInstance()
if input == "" {
fanInChan := make(chan SearchResult, 2)
// return all grid tagname
go getAllKeyByGridLevel(ctx, rdb, fanInChan)
// return all component nspath
go getAllKeyByNSPathLevel(ctx, rdb, fanInChan)
results := make(map[string]SearchResult)
for range 2 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "return all keys at the special level from redis failed", "recommend_type", result.RecommendType, "error", result.Err)
continue
}
if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
filterResults := make([]string, 0, len(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
}
return results
}
inputSlice := strings.Split(input, ".")
inputSliceLen := len(inputSlice)
fanInChan := make(chan SearchResult, 4)
switch inputSliceLen {
case 1:
searchInput := inputSlice[0]
// grid tagname search
go levelOneRedisSearch(ctx, rdb, constants.GridRecommendHierarchyType, constants.FullRecommendLength, searchInput, constants.RedisAllGridSetKey, fanInChan)
// component nspath search
go levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, searchInput, constants.RedisAllCompNSPathSetKey, fanInChan)
results := make(map[string]SearchResult)
// TODO 后续根据支持的数据标识语法长度,进行值的变更
for range 2 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "exec redis fuzzy search by key failed", "recommend_type", result.RecommendType, "error", result.Err)
continue
}
if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
filterResults := make([]string, 0, len(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
}
return results
case 2:
// zone tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllZoneSetKey, inputSlice, fanInChan)
// component tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllCompTagSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 2 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
case 3:
// station tanname search
go handleLevelFuzzySearch(ctx, rdb, constants.StationRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllStationSetKey, inputSlice, fanInChan)
// config search
go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 2 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
case 4:
// component nspath search
go handleLevelFuzzySearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompNSPathSetKey, inputSlice, fanInChan)
// measurement tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, constants.RedisAllConfigSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 2 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
case 5:
// component tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllCompTagSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 1 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
case 6:
// config search
go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllConfigSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 1 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
case 7:
// measurement tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, constants.RedisAllMeasTagSetKey, inputSlice, fanInChan)
results := make(map[string]SearchResult)
for range 1 {
result := <-fanInChan
if result.Err != nil {
logger.Error(ctx, "query all keys at the special level from redis failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
results[result.RecommendType.String()] = result
}
return results
default:
logger.Error(ctx, "unsupport length of search input", "input_len", inputSliceLen)
return nil
}
}
func queryMemberFromSpecificsLevel(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, keyPrefix string) ([]string, error) {
queryKey := getSpecificKeyByLength(hierarchy, keyPrefix)
return rdb.SMembers(ctx, queryKey).Result()
}
func getAllKeyByGridLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) {
setKey := constants.RedisAllGridSetKey
hierarchy := constants.GridRecommendHierarchyType
members, err := rdb.SMembers(ctx, setKey).Result()
if err != nil && err != redigo.ErrNil {
logger.Error(ctx, "get all members from redis by special key failed", "key", setKey, "op", "SMembers", "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: members,
IsFuzzy: false,
Err: nil,
}
}
func getAllKeyByNSPathLevel(ctx context.Context, rdb *redis.Client, fanInChan chan SearchResult) {
queryKey := constants.RedisAllCompNSPathSetKey
hierarchy := constants.CompNSPathRecommendHierarchyType
members, err := rdb.SMembers(ctx, queryKey).Result()
if err != nil && err != redigo.ErrNil {
logger.Error(ctx, "get all members by special key failed", "key", queryKey, "op", "SMembers", "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: members,
IsFuzzy: false,
Err: nil,
}
}
func combineQueryResultByInput(hierarchy constants.RecommendHierarchyType, recommendLenType string, inputSlice []string, queryResults []string) []string {
prefixs := make([]string, 0, len(inputSlice))
recommandResults := make([]string, 0, len(queryResults))
switch recommendLenType {
case constants.FullRecommendLength:
switch hierarchy {
case constants.ZoneRecommendHierarchyType:
prefixs = []string{inputSlice[0]}
case constants.StationRecommendHierarchyType:
prefixs = inputSlice[0:2]
case constants.CompNSPathRecommendHierarchyType:
prefixs = inputSlice[0:3]
case constants.CompTagRecommendHierarchyType:
prefixs = inputSlice[0:4]
case constants.ConfigRecommendHierarchyType:
prefixs = inputSlice[0:5]
case constants.MeasTagRecommendHierarchyType:
prefixs = inputSlice[0:6]
default:
return []string{}
}
case constants.IsLocalRecommendLength:
switch hierarchy {
case constants.CompTagRecommendHierarchyType:
prefixs = []string{inputSlice[0]}
case constants.ConfigRecommendHierarchyType:
prefixs = inputSlice[0:2]
case constants.MeasTagRecommendHierarchyType:
prefixs = inputSlice[0:3]
default:
return []string{}
}
}
for _, queryResult := range queryResults {
combineStrs := make([]string, 0, len(inputSlice))
combineStrs = append(combineStrs, prefixs...)
combineStrs = append(combineStrs, queryResult)
recommandResult := strings.Join(combineStrs, ".")
recommandResults = append(recommandResults, recommandResult)
}
return recommandResults
}
func getSpecificKeyByLength(hierarchy constants.RecommendHierarchyType, keyPrefix string) string {
switch hierarchy {
case constants.GridRecommendHierarchyType:
return constants.RedisAllGridSetKey
case constants.ZoneRecommendHierarchyType:
return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, keyPrefix)
case constants.StationRecommendHierarchyType:
return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, keyPrefix)
case constants.CompNSPathRecommendHierarchyType:
return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, keyPrefix)
case constants.CompTagRecommendHierarchyType:
return fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, keyPrefix)
case constants.ConfigRecommendHierarchyType:
return constants.RedisAllConfigSetKey
case constants.MeasTagRecommendHierarchyType:
return fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, keyPrefix)
default:
return constants.RedisAllGridSetKey
}
}
// handleLevelFuzzySearch define func to process recommendation logic for specific levels(level >= 2)
func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy constants.RecommendHierarchyType, recommendLenType string, redisSetKey string, inputSlice []string, fanInChan chan SearchResult) {
inputSliceLen := len(inputSlice)
searchInputIndex := inputSliceLen - 1
searchInput := inputSlice[searchInputIndex]
searchPrefix := strings.Join(inputSlice[0:searchInputIndex], ".")
if searchInput == "" {
var specificalKey string
specificalKeyIndex := searchInputIndex - 1
if hierarchy == constants.MeasTagRecommendHierarchyType {
specificalKeyIndex = searchInputIndex - 2
}
if specificalKeyIndex >= 0 {
specificalKey = inputSlice[specificalKeyIndex]
}
if recommendLenType == constants.IsLocalRecommendLength && hierarchy == constants.ConfigRecommendHierarchyType {
// token4-token7 model and query all config keys
redisSetKey = constants.RedisAllCompTagSetKey
keyExists, err := rdb.SIsMember(ctx, redisSetKey, specificalKey).Result()
if err != nil {
logger.Error(ctx, "check key exist from redis set failed ", "key", redisSetKey, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
if !keyExists {
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: []string{},
IsFuzzy: false,
Err: nil,
}
return
}
}
members, err := queryMemberFromSpecificsLevel(ctx, rdb, hierarchy, specificalKey)
if err != nil && err != redis.Nil {
logger.Error(ctx, "query members from redis by special key failed", "key", specificalKey, "member", searchInput, "op", "SMember", "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
recommandResults := combineQueryResultByInput(hierarchy, recommendLenType, inputSlice, members)
if hierarchy == constants.ConfigRecommendHierarchyType || hierarchy == constants.MeasTagRecommendHierarchyType {
// 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))
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
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: recommandResults,
IsFuzzy: false,
Err: nil,
}
return
}
keyExists, err := rdb.SIsMember(ctx, redisSetKey, searchInput).Result()
if err != nil {
logger.Error(ctx, "check key exist from redis set failed ", "key", redisSetKey, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
if keyExists {
var QueryData []string
if hierarchy == constants.MaxIdentifyHierarchy || (hierarchy == constants.IdentifyHierarchy && redisSetKey == constants.RedisAllMeasTagSetKey) {
QueryData = []string{""}
} else {
QueryData = []string{"."}
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: QueryData,
IsFuzzy: false,
Err: nil,
}
return
}
// start redis fuzzy search
recommends, err := runFuzzySearch(ctx, rdb, searchInput, searchPrefix, hierarchy, recommendLenType)
if err != nil {
logger.Error(ctx, "fuzzy search failed by hierarchy", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: nil,
IsFuzzy: false,
Err: err,
}
return
}
if len(recommends) == 0 {
logger.Info(ctx, "fuzzy search without result", "hierarchy", hierarchy, "search_input", searchInput, "error", err)
}
fanInChan <- SearchResult{
RecommendType: hierarchy,
QueryDatas: recommends,
IsFuzzy: true,
Err: nil,
}
return
}
// 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) {
var configToken string
var comparePrefix string
searchInputLen := len(searchInput)
compareHierarchyLen := int(hierarchy)
comparePrefix = searchPrefix
if hierarchy == constants.MeasTagRecommendHierarchyType {
compareHierarchyLen = int(hierarchy) - 1
lastDotIndex := strings.LastIndex(searchPrefix, ".")
if lastDotIndex == -1 {
configToken = ""
} else {
configToken = searchPrefix[lastDotIndex+1:]
comparePrefix = searchPrefix[:lastDotIndex]
}
}
for searchInputLen != 0 {
fuzzyInput := searchInput
if comparePrefix != "" {
fuzzyInput = comparePrefix + "." + searchInput
}
results, err := ac.SuggestOpts(fuzzyInput, redisearch.SuggestOptions{
Num: math.MaxInt16,
Fuzzy: true,
WithScores: false,
WithPayloads: false,
})
if err != nil {
logger.Error(ctx, "query key by redis fuzzy search failed", "query_key", fuzzyInput, "error", err)
return nil, fmt.Errorf("redisearch suggest failed: %w", err)
}
if len(results) == 0 {
// 如果没有结果,退一步(删除最后一个字节)并继续循环
// TODO 考虑使用其他方式代替 for 循环退一字节的查询方式
searchInput = searchInput[:len(searchInput)-1]
searchInputLen = len(searchInput)
continue
}
var recommends []string
for _, result := range results {
term := result.Term
var termHierarchyLen int
var termPrefix string
var termLastPart string
lastDotIndex := strings.LastIndex(term, ".")
if lastDotIndex == -1 {
termPrefix = ""
termLastPart = term
} else {
termPrefix = term[:lastDotIndex]
termLastPart = term[lastDotIndex+1:]
}
if recommendLenType == constants.FullRecommendLength && hierarchy == constants.GridRecommendHierarchyType {
exists, err := rdb.SIsMember(ctx, constants.RedisAllGridSetKey, termLastPart).Result()
if err != nil || !exists {
logger.Info(ctx, "query key by redis fuzzy search failed or term not in redis all grid set", "query_key", fuzzyInput, "exists", exists, "error", err)
continue
}
}
if recommendLenType == constants.FullRecommendLength {
if result.Term == "" {
termHierarchyLen = 1
} else {
termHierarchyLen = strings.Count(result.Term, ".") + 1
}
} else if recommendLenType == constants.IsLocalRecommendLength {
if result.Term == "" {
termHierarchyLen = 4
} else {
termHierarchyLen = strings.Count(result.Term, ".") + 4
}
}
if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings.HasPrefix(termLastPart, searchInput) {
recommend := result.Term
if hierarchy == constants.MeasTagRecommendHierarchyType {
recommend = strings.Join([]string{termPrefix, configToken, termLastPart}, ".")
}
recommends = append(recommends, recommend)
}
}
return recommends, nil
}
return []string{}, nil
}