modelRT/model/redis_recommend.go

510 lines
15 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, 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, searchInput, "", hierarchy)
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", "query_key", result.RecommendType, "error", result.Err)
continue
}
if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
// TODO 增加 nspath 过滤
fmt.Println("process nspath")
}
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, searchInput, constants.RedisAllGridSetKey, fanInChan)
// component nspath search
go levelOneRedisSearch(ctx, rdb, constants.CompNSPathRecommendHierarchyType, 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 :%s failed", "query_key", result.RecommendType, "error", result.Err)
continue
}
if result.RecommendType == constants.CompNSPathRecommendHierarchyType {
// TODO 增加 nspath 过滤
fmt.Println("process nspath")
}
results[result.RecommendType.String()] = result
}
return results
case 2:
// zone tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.ZoneRecommendHierarchyType, constants.RedisAllZoneSetKey, inputSlice, fanInChan)
// component tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.CompTagRecommendHierarchyType, 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.RedisAllStationSetKey, inputSlice, fanInChan)
// config search
go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, 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.RedisAllCompNSPathSetKey, inputSlice, fanInChan)
// measurement tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, 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.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 6:
// config search
go handleLevelFuzzySearch(ctx, rdb, constants.ConfigRecommendHierarchyType, 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 7:
// measurement tagname search
go handleLevelFuzzySearch(ctx, rdb, constants.MeasTagRecommendHierarchyType, constants.RedisAllMeasTagSetKey, 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
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) {
queryKey := constants.RedisAllGridSetKey
hierarchy := constants.GridRecommendHierarchyType
members, err := rdb.SMembers(ctx, queryKey).Result()
if err != nil {
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 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 {
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, inputSlice []string, queryResults []string) []string {
prefixs := make([]string, 0, len(inputSlice))
recommandResults := make([]string, 0, len(queryResults))
switch hierarchy {
// TODO 优化 case 为常量
case 2:
prefixs = []string{inputSlice[0]}
case 3:
prefixs = inputSlice[0:2]
case 4:
prefixs = inputSlice[0:3]
case 5:
prefixs = inputSlice[0:4]
case 6:
prefixs = inputSlice[0:5]
case 7:
prefixs = inputSlice[0:6]
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 {
// TODO 优化 case 为常量
case 1:
return constants.RedisAllGridSetKey
case 2:
return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, keyPrefix)
case 3:
return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, keyPrefix)
case 4:
return fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, keyPrefix)
case 5:
return fmt.Sprintf(constants.RedisSpecStationCompTagSetKey, keyPrefix)
case 6:
return constants.RedisAllConfigSetKey
case 7:
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, 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 specificalKeyIndex >= 0 {
specificalKey = inputSlice[specificalKeyIndex]
}
members, err := queryMemberFromSpecificsLevel(ctx, rdb, hierarchy, specificalKey)
if err != 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, inputSlice, members)
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, searchInput, searchPrefix, hierarchy)
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.Error(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, searchInput string, searchPrefix string, hierarchy constants.RecommendHierarchyType) ([]string, error) {
searchInputLen := len(searchInput)
for searchInputLen != 0 {
results, err := ac.SuggestOpts(searchInput, 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", searchInput, "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 termSliceLen int
var termPrefix string
lastDotIndex := strings.LastIndex(term, ".")
if lastDotIndex == -1 {
termPrefix = ""
} else {
termPrefix = term[:lastDotIndex]
}
if result.Term == "" {
termSliceLen = 1
} else {
termSliceLen = strings.Count(result.Term, ".") + 1
}
if termSliceLen == int(hierarchy) && termPrefix == searchPrefix {
recommends = append(recommends, result.Term)
}
}
return recommends, nil
}
return []string{}, nil
}