2025-09-24 16:43:11 +08:00
// Package model define model struct of model runtime service
package model
import (
"context"
2025-12-15 16:49:38 +08:00
"errors"
2025-09-24 16:43:11 +08:00
"fmt"
2025-10-15 17:08:32 +08:00
"math"
2026-07-09 16:31:19 +08:00
"slices"
2025-12-23 14:52:39 +08:00
"strings"
2026-07-08 14:41:13 +08:00
"sync"
2025-12-23 14:52:39 +08:00
2025-09-24 16:43:11 +08:00
"modelRT/constants"
"modelRT/diagram"
2025-09-25 16:39:45 +08:00
"modelRT/logger"
2025-12-15 16:49:38 +08:00
"modelRT/util"
2025-09-24 16:43:11 +08:00
2025-10-15 17:08:32 +08:00
"github.com/RediSearch/redisearch-go/v2/redisearch"
redigo "github.com/gomodule/redigo/redis"
2025-12-03 16:55:14 +08:00
"github.com/redis/go-redis/v9"
2026-07-08 14:41:13 +08:00
"golang.org/x/sync/errgroup"
2025-09-24 16:43:11 +08:00
)
2025-12-15 16:49:38 +08:00
// 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
2026-07-09 16:31:19 +08:00
IsFuzzy bool // 代表本次搜索是否是模糊搜索, 例如输入 grid1.
2026-06-30 13:55:42 +08:00
IsFallback bool
2026-06-29 14:13:58 +08:00
Offset int
2025-12-15 16:49:38 +08:00
Err error
}
2025-10-15 17:08:32 +08:00
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 )
}
2026-07-08 14:41:13 +08:00
type recommendSearchJob func ( ) SearchResult
func runRecommendSearches ( ctx context . Context , logMsg string , filterCompNSPath bool , jobs ... recommendSearchJob ) map [ string ] SearchResult {
results := make ( map [ string ] SearchResult )
var mu sync . Mutex
var group errgroup . Group
for _ , job := range jobs {
job := job
group . Go ( func ( ) error {
result := job ( )
if result . Err != nil {
return fmt . Errorf ( "%s: %w" , result . RecommendType , result . Err )
}
if filterCompNSPath && result . RecommendType == constants . CompNSPathRecommendHierarchyType {
result . QueryDatas = filterLocalNSPathResults ( result . QueryDatas )
}
mu . Lock ( )
results [ result . RecommendType . String ( ) ] = result
mu . Unlock ( )
return nil
} )
}
if err := group . Wait ( ) ; err != nil {
logger . Error ( ctx , logMsg , "error" , err )
}
return results
}
2026-07-07 17:11:25 +08:00
// levelOneRedisSearch 处理第一段输入的搜索, 例如 grid 或本地 nspath。
2026-07-08 14:41:13 +08:00
func levelOneRedisSearch ( ctx context . Context , rdb * redis . Client , hierarchy constants . RecommendHierarchyType , recommendLenType string , searchInput string , searchRedisKey string ) ( result SearchResult ) {
2025-12-15 16:49:38 +08:00
defer func ( ) {
if r := recover ( ) ; r != nil {
logger . Error ( ctx , "searchFunc panicked" , "panic" , r )
2026-07-08 14:41:13 +08:00
result = SearchResult {
2025-12-15 16:49:38 +08:00
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 )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
// the input key is the complete hierarchical value
if exists {
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : [ ] string { "." } ,
IsFuzzy : false ,
Err : nil ,
}
}
// process fuzzy search result
2026-06-29 14:13:58 +08:00
recommends , offset , err := runFuzzySearch ( ctx , rdb , searchInput , "" , hierarchy , recommendLenType )
2025-12-15 16:49:38 +08:00
if err != nil {
logger . Error ( ctx , fmt . Sprintf ( "fuzzy search failed for %s hierarchical" , util . GetLevelStrByRdsKey ( searchRedisKey ) ) , "search_input" , searchInput , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
if len ( recommends ) > 0 {
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : recommends ,
IsFuzzy : true ,
2026-06-29 14:13:58 +08:00
Offset : offset ,
2025-12-15 16:49:38 +08:00
Err : nil ,
}
}
2026-06-30 13:55:42 +08:00
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 )
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-06-30 13:55:42 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : true ,
IsFallback : true ,
Err : err ,
}
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
2026-06-30 13:55:42 +08:00
QueryDatas : recommends ,
2025-12-15 16:49:38 +08:00
IsFuzzy : true ,
2026-06-30 13:55:42 +08:00
IsFallback : true ,
Offset : 0 ,
2025-12-15 16:49:38 +08:00
Err : nil ,
}
}
2025-09-25 16:39:45 +08:00
// RedisSearchRecommend define func of redis search by input string and return recommend results
2025-12-15 16:49:38 +08:00
func RedisSearchRecommend ( ctx context . Context , input string ) map [ string ] SearchResult {
2025-09-24 17:26:46 +08:00
rdb := diagram . GetRedisClientInstance ( )
2026-07-08 14:41:13 +08:00
finalizeResults := func ( normalizeInput string , results map [ string ] SearchResult ) map [ string ] SearchResult {
2026-07-03 15:59:53 +08:00
results = suppressFallbackResultsWhenConcreteHitExists ( results )
2026-07-08 14:41:13 +08:00
return normalizeRecommendResults ( normalizeInput , results )
2026-06-29 14:13:58 +08:00
}
2025-09-24 17:26:46 +08:00
2026-07-08 14:41:13 +08:00
if input == "" || shouldFallbackToInitialRecommend ( input ) {
results := runRecommendSearches ( ctx , "return all keys at the special level from redis failed" , true ,
func ( ) SearchResult {
return getAllKeyByGridLevel ( ctx , rdb )
} ,
func ( ) SearchResult {
return getAllKeyByNSPathLevel ( ctx , rdb )
} ,
)
return finalizeResults ( "" , results )
2025-12-04 17:26:35 +08:00
}
inputSlice := strings . Split ( input , "." )
inputSliceLen := len ( inputSlice )
switch inputSliceLen {
case 1 :
2025-12-15 16:49:38 +08:00
searchInput := inputSlice [ 0 ]
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "exec redis fuzzy search by key failed" , true ,
func ( ) SearchResult {
return levelOneRedisSearch ( ctx , rdb , constants . GridRecommendHierarchyType , constants . FullRecommendLength , searchInput , constants . RedisAllGridSetKey )
} ,
func ( ) SearchResult {
return levelOneRedisSearch ( ctx , rdb , constants . CompNSPathRecommendHierarchyType , constants . IsLocalRecommendLength , searchInput , constants . RedisAllCompNSPathSetKey )
} ,
)
return finalizeResults ( input , results )
2025-12-15 16:49:38 +08:00
case 2 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . ZoneRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllZoneSetKey , inputSlice )
} ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . CompTagRecommendHierarchyType , constants . IsLocalRecommendLength , constants . RedisAllCompTagSetKey , inputSlice )
} ,
func ( ) SearchResult {
return handleNSPathMeasSearch ( ctx , rdb , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-15 16:49:38 +08:00
case 3 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . StationRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllStationSetKey , inputSlice )
} ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . ConfigRecommendHierarchyType , constants . IsLocalRecommendLength , constants . RedisAllConfigSetKey , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-15 16:49:38 +08:00
case 4 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . CompNSPathRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllCompNSPathSetKey , inputSlice )
} ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . MeasTagRecommendHierarchyType , constants . IsLocalRecommendLength , constants . RedisAllConfigSetKey , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-04 17:26:35 +08:00
case 5 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . CompTagRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllCompTagSetKey , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-04 17:26:35 +08:00
case 6 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . ConfigRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllConfigSetKey , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-04 17:26:35 +08:00
case 7 :
2026-07-08 14:41:13 +08:00
results := runRecommendSearches ( ctx , "query all keys at the special level from redis failed" , false ,
func ( ) SearchResult {
return handleLevelFuzzySearch ( ctx , rdb , constants . MeasTagRecommendHierarchyType , constants . FullRecommendLength , constants . RedisAllMeasTagSetKey , inputSlice )
} ,
)
return finalizeResults ( input , results )
2025-12-04 17:26:35 +08:00
default :
logger . Error ( ctx , "unsupport length of search input" , "input_len" , inputSliceLen )
2025-12-15 16:49:38 +08:00
return nil
2025-12-04 17:26:35 +08:00
}
}
2026-07-08 14:41:13 +08:00
func shouldFallbackToInitialRecommend ( input string ) bool {
return strings . HasPrefix ( input , "." )
}
2026-07-07 17:11:25 +08:00
// suppressFallbackResultsWhenConcreteHitExists 有明确命中时丢弃兜底结果, 避免返回过宽推荐。
2026-07-03 15:59:53 +08:00
func suppressFallbackResultsWhenConcreteHitExists ( results map [ string ] SearchResult ) map [ string ] SearchResult {
hasConcreteHit := false
for _ , result := range results {
if result . Err == nil && ! result . IsFallback && len ( result . QueryDatas ) > 0 {
hasConcreteHit = true
break
}
}
if ! hasConcreteHit {
return results
}
for key , result := range results {
if result . IsFallback {
delete ( results , key )
}
}
return results
}
2026-07-07 17:11:25 +08:00
// normalizeRecommendResults 统一处理 offset、去重和裁剪, 让响应只返回用户还没输入的部分。
2026-06-29 14:13:58 +08:00
func normalizeRecommendResults ( input string , results map [ string ] SearchResult ) map [ string ] SearchResult {
for key , result := range results {
if result . Err != nil {
continue
}
if ! result . IsFuzzy {
2026-07-09 16:31:19 +08:00
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 )
}
2026-06-29 14:13:58 +08:00
}
result . QueryDatas = trimRecommendTerms ( result . QueryDatas , result . Offset )
results [ key ] = result
}
return results
}
2026-07-09 16:31:19 +08:00
// hasExactCompletion 判断推荐项是否表示当前输入已经完整命中, 无需再补充后缀。
func hasExactCompletion ( recommends [ ] string ) bool {
return slices . Contains ( recommends , "" )
}
// hasLevelContinuation 判断推荐项是否表示当前层级已命中, 可继续补充分隔符进入下一层。
func hasLevelContinuation ( recommends [ ] string ) bool {
return slices . Contains ( recommends , "." )
}
2026-07-07 17:11:25 +08:00
// calcSearchResultOffset 计算 input 与推荐项的最短公共前缀长度, 用作裁剪位置。
2026-06-29 14:13:58 +08:00
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
}
2026-07-09 16:31:19 +08:00
// recommendPrefixOffset 返回推荐结果中已由输入确定的前缀长度, 包含末尾分隔符。
func recommendPrefixOffset ( prefixTokens [ ] string ) int {
prefix := strings . Join ( prefixTokens , "." )
if prefix == "" {
return 0
2026-06-30 13:55:42 +08:00
}
2026-07-09 16:31:19 +08:00
return len ( [ ] rune ( prefix ) ) + 1
2026-06-30 13:55:42 +08:00
}
2026-07-07 17:11:25 +08:00
// trimRecommendTerms 按 offset 截掉已输入前缀, 并去掉空值和重复值。
2026-06-29 14:13:58 +08:00
func trimRecommendTerms ( recommends [ ] string , offset int ) [ ] string {
resultRecommends := make ( [ ] string , 0 , len ( recommends ) )
seen := make ( map [ string ] struct { } )
for _ , recommend := range recommends {
2026-06-30 13:55:42 +08:00
recommendRunes := [ ] rune ( recommend )
if offset > len ( recommendRunes ) {
2026-06-29 14:13:58 +08:00
continue
}
2026-06-30 13:55:42 +08:00
recommendTerm := string ( recommendRunes [ offset : ] )
2026-06-29 14:13:58 +08:00
if recommendTerm == "" {
continue
}
if _ , exists := seen [ recommendTerm ] ; exists {
continue
}
seen [ recommendTerm ] = struct { } { }
resultRecommends = append ( resultRecommends , recommendTerm )
}
return resultRecommends
}
2026-07-07 17:11:25 +08:00
// filterLocalNSPathResults 只保留本地组件 nspath, 避免远端 nspath 进入本地简写推荐。
2026-06-30 13:55:42 +08:00
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
}
2026-07-07 17:11:25 +08:00
// queryMemberFromSpecificsLevel 根据上一层级 token 查询其真实下级集合。
2025-12-15 16:49:38 +08:00
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 ( )
}
2026-07-07 17:11:25 +08:00
// fallbackAllCurrentLevel 兜底返回当前层级的全量集合。
2026-06-30 13:55:42 +08:00
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
}
2026-07-07 17:11:25 +08:00
// fallbackCurrentLevelByInput 兜底时先校验上下级关系, 避免错误前缀返回全层级数据。
func fallbackCurrentLevelByInput ( ctx context . Context , rdb * redis . Client , hierarchy constants . RecommendHierarchyType , inputSlice [ ] string ) ( [ ] string , error ) {
allSetKey , ok := getAllSetKeyByHierarchy ( hierarchy )
if ! ok {
return nil , fmt . Errorf ( "unsupported recommend hierarchy for fallback: %s" , hierarchy )
}
specificSetKey , ok := fallbackSpecificSetKey ( hierarchy , inputSlice )
if ! ok {
return fallbackAllCurrentLevel ( ctx , rdb , hierarchy )
}
count , err := rdb . SInterCard ( ctx , 1 , allSetKey , specificSetKey ) . Result ( )
if err != nil && err != redis . Nil {
return nil , err
}
if count == 0 {
return [ ] string { } , nil
}
members , err := rdb . SMembers ( ctx , specificSetKey ) . Result ( )
if err != nil && err != redis . Nil {
return nil , err
}
return members , nil
}
// fallbackSpecificSetKey 根据输入切片推导 fallback 应该校验的上一层级 specific set。
func fallbackSpecificSetKey ( hierarchy constants . RecommendHierarchyType , inputSlice [ ] string ) ( string , bool ) {
if len ( inputSlice ) == 0 {
return "" , false
}
searchInputIndex := len ( inputSlice ) - 1
specificKeyIndex := searchInputIndex - 1
if hierarchy == constants . MeasTagRecommendHierarchyType {
specificKeyIndex = searchInputIndex - 2
}
if specificKeyIndex < 0 || specificKeyIndex >= len ( inputSlice ) || inputSlice [ specificKeyIndex ] == "" {
return "" , false
}
switch hierarchy {
case constants . ZoneRecommendHierarchyType ,
constants . StationRecommendHierarchyType ,
constants . CompNSPathRecommendHierarchyType ,
constants . CompTagRecommendHierarchyType ,
constants . MeasTagRecommendHierarchyType :
return getSpecificKeyByLength ( hierarchy , inputSlice [ specificKeyIndex ] ) , true
default :
return "" , false
}
}
// getAllSetKeyByHierarchy 返回某个推荐层级对应的全量 Redis set key。
2026-06-30 13:55:42 +08:00
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
}
}
2026-07-07 17:11:25 +08:00
// getAllKeyByGridLevel 返回所有 grid tag, 用于空输入时的初始推荐。
2026-07-08 14:41:13 +08:00
func getAllKeyByGridLevel ( ctx context . Context , rdb * redis . Client ) SearchResult {
2025-12-16 16:34:19 +08:00
setKey := constants . RedisAllGridSetKey
2025-12-15 16:49:38 +08:00
hierarchy := constants . GridRecommendHierarchyType
2025-12-16 16:34:19 +08:00
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 )
2025-12-15 16:49:38 +08:00
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : members ,
IsFuzzy : false ,
Err : nil ,
}
}
2026-07-07 17:11:25 +08:00
// getAllKeyByNSPathLevel 返回所有组件 nspath, 用于空输入时的初始推荐。
2026-07-08 14:41:13 +08:00
func getAllKeyByNSPathLevel ( ctx context . Context , rdb * redis . Client ) SearchResult {
2025-12-15 16:49:38 +08:00
queryKey := constants . RedisAllCompNSPathSetKey
hierarchy := constants . CompNSPathRecommendHierarchyType
members , err := rdb . SMembers ( ctx , queryKey ) . Result ( )
2025-12-16 16:34:19 +08:00
if err != nil && err != redigo . ErrNil {
2025-12-15 16:49:38 +08:00
logger . Error ( ctx , "get all members by special key failed" , "key" , queryKey , "op" , "SMembers" , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : members ,
IsFuzzy : false ,
Err : nil ,
2025-12-04 17:26:35 +08:00
}
}
2026-07-07 17:11:25 +08:00
// combineQueryResultByInput 把当前层级查询结果和输入前缀拼成完整推荐路径。
2025-12-22 10:45:47 +08:00
func combineQueryResultByInput ( hierarchy constants . RecommendHierarchyType , recommendLenType string , inputSlice [ ] string , queryResults [ ] string ) [ ] string {
2025-12-04 17:26:35 +08:00
prefixs := make ( [ ] string , 0 , len ( inputSlice ) )
recommandResults := make ( [ ] string , 0 , len ( queryResults ) )
2025-12-22 10:45:47 +08:00
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 { }
}
2025-12-04 17:26:35 +08:00
}
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
}
2026-07-09 16:31:19 +08:00
// 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 != ""
}
2026-07-07 17:11:25 +08:00
// getSpecificKeyByLength 根据层级和父级 token 生成对应的 specific set key。
2025-12-15 16:49:38 +08:00
func getSpecificKeyByLength ( hierarchy constants . RecommendHierarchyType , keyPrefix string ) string {
switch hierarchy {
2025-12-22 17:38:15 +08:00
case constants . GridRecommendHierarchyType :
2025-12-04 17:26:35 +08:00
return constants . RedisAllGridSetKey
2025-12-22 17:38:15 +08:00
case constants . ZoneRecommendHierarchyType :
2025-12-15 16:49:38 +08:00
return fmt . Sprintf ( constants . RedisSpecGridZoneSetKey , keyPrefix )
2025-12-22 17:38:15 +08:00
case constants . StationRecommendHierarchyType :
2025-12-15 16:49:38 +08:00
return fmt . Sprintf ( constants . RedisSpecZoneStationSetKey , keyPrefix )
2025-12-22 17:38:15 +08:00
case constants . CompNSPathRecommendHierarchyType :
2025-12-15 16:49:38 +08:00
return fmt . Sprintf ( constants . RedisSpecStationCompNSPATHSetKey , keyPrefix )
2025-12-22 17:38:15 +08:00
case constants . CompTagRecommendHierarchyType :
2025-12-24 16:55:55 +08:00
return fmt . Sprintf ( constants . RedisSpecCompNSPathCompTagSetKey , keyPrefix )
2025-12-22 17:38:15 +08:00
case constants . ConfigRecommendHierarchyType :
2025-12-04 17:26:35 +08:00
return constants . RedisAllConfigSetKey
2025-12-22 17:38:15 +08:00
case constants . MeasTagRecommendHierarchyType :
2025-12-15 16:49:38 +08:00
return fmt . Sprintf ( constants . RedisSpecCompTagMeasSetKey , keyPrefix )
2025-12-04 17:26:35 +08:00
default :
return constants . RedisAllGridSetKey
}
}
2026-07-07 17:11:25 +08:00
// handleNSPathMeasSearch 处理 nspath.measurement 的简写搜索路径。
2026-07-08 14:41:13 +08:00
func handleNSPathMeasSearch ( ctx context . Context , rdb * redis . Client , inputSlice [ ] string ) SearchResult {
2026-07-02 13:49:47 +08:00
hierarchy := constants . MeasTagRecommendHierarchyType
nsPath := inputSlice [ 0 ]
searchInput := inputSlice [ 1 ]
nsPathExists , err := rdb . SIsMember ( ctx , constants . RedisAllCompNSPathSetKey , nsPath ) . Result ( )
if err != nil {
logger . Error ( ctx , "check nspath exist from redis set failed" , "key" , constants . RedisAllCompNSPathSetKey , "nspath" , nsPath , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
if ! nsPathExists {
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : [ ] string { } ,
IsFuzzy : false ,
Err : nil ,
}
}
queryKey := fmt . Sprintf ( constants . RedisSpecCompNSPathMeasSetKey , nsPath )
if searchInput == "" {
members , err := rdb . SMembers ( ctx , queryKey ) . Result ( )
if err != nil && err != redis . Nil {
logger . Error ( ctx , "query measurement tags by nspath failed" , "key" , queryKey , "nspath" , nsPath , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : combineNSPathMeasResults ( nsPath , members ) ,
IsFuzzy : false ,
Err : nil ,
}
}
measTagExists , err := rdb . SIsMember ( ctx , queryKey , searchInput ) . Result ( )
if err != nil {
logger . Error ( ctx , "check measurement tag exist from nspath set failed" , "key" , queryKey , "meas_tag" , searchInput , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
if measTagExists {
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : [ ] string { "" } ,
IsFuzzy : false ,
Err : nil ,
}
}
recommends , offset , err := runNSPathMeasFuzzySearch ( ctx , nsPath , searchInput )
if err != nil {
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : true ,
Err : err ,
}
}
2026-07-09 16:31:19 +08:00
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
}
2026-07-02 13:49:47 +08:00
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-07-02 13:49:47 +08:00
RecommendType : hierarchy ,
QueryDatas : recommends ,
IsFuzzy : true ,
2026-07-09 16:31:19 +08:00
IsFallback : isFallback ,
2026-07-02 13:49:47 +08:00
Offset : offset ,
Err : nil ,
}
}
2026-07-07 17:11:25 +08:00
// combineNSPathMeasResults 把 nspath 与 measurement tag 拼成简写推荐结果。
2026-07-02 13:49:47 +08:00
func combineNSPathMeasResults ( nsPath string , measTags [ ] string ) [ ] string {
results := make ( [ ] string , 0 , len ( measTags ) )
for _ , measTag := range measTags {
results = append ( results , fmt . Sprintf ( "%s.%s" , nsPath , measTag ) )
}
return results
}
2026-07-07 17:11:25 +08:00
// runNSPathMeasFuzzySearch 对 nspath.measurement 简写做模糊搜索, 无结果时逐字回退。
2026-07-02 13:49:47 +08:00
func runNSPathMeasFuzzySearch ( ctx context . Context , nsPath string , searchInput string ) ( [ ] string , int , error ) {
searchInputLen := len ( [ ] rune ( searchInput ) )
for searchInputLen != 0 {
fuzzyInput := fmt . Sprintf ( "%s.%s" , nsPath , searchInput )
results , err := ac . SuggestOpts ( fuzzyInput , redisearch . SuggestOptions {
Num : math . MaxInt16 ,
Fuzzy : true ,
WithScores : false ,
WithPayloads : false ,
} )
if err != nil {
logger . Error ( ctx , "query measurement tag by nspath fuzzy search failed" , "query_key" , fuzzyInput , "error" , err )
return nil , 0 , fmt . Errorf ( "redisearch suggest failed: %w" , err )
}
if len ( results ) == 0 {
searchInput = trimLastRune ( searchInput )
searchInputLen = len ( [ ] rune ( searchInput ) )
continue
}
recommends := make ( [ ] string , 0 , len ( results ) )
for _ , result := range results {
termPrefix , termLastPart := splitLastToken ( result . Term )
if termPrefix == nsPath && strings . HasPrefix ( termLastPart , searchInput ) {
recommends = append ( recommends , result . Term )
}
}
if len ( recommends ) == 0 {
searchInput = trimLastRune ( searchInput )
searchInputLen = len ( [ ] rune ( searchInput ) )
continue
}
return recommends , len ( [ ] rune ( fmt . Sprintf ( "%s.%s" , nsPath , searchInput ) ) ) , nil
}
return [ ] string { } , 0 , nil
}
2026-07-07 17:11:25 +08:00
// splitLastToken 拆分最后一个点号后的 token, 返回前缀和末级 token。
2026-07-02 13:49:47 +08:00
func splitLastToken ( term string ) ( string , string ) {
lastDotIndex := strings . LastIndex ( term , "." )
if lastDotIndex == - 1 {
return "" , term
}
return term [ : lastDotIndex ] , term [ lastDotIndex + 1 : ]
}
2026-07-07 17:11:25 +08:00
// handleLevelFuzzySearch 处理第二层及以后层级的精确、模糊和兜底推荐。
2026-07-08 14:41:13 +08:00
func handleLevelFuzzySearch ( ctx context . Context , rdb * redis . Client , hierarchy constants . RecommendHierarchyType , recommendLenType string , redisSetKey string , inputSlice [ ] string ) SearchResult {
2025-12-15 16:49:38 +08:00
inputSliceLen := len ( inputSlice )
searchInputIndex := inputSliceLen - 1
2025-12-04 17:26:35 +08:00
searchInput := inputSlice [ searchInputIndex ]
2025-12-06 18:32:00 +08:00
searchPrefix := strings . Join ( inputSlice [ 0 : searchInputIndex ] , "." )
2025-12-04 17:26:35 +08:00
if searchInput == "" {
var specificalKey string
specificalKeyIndex := searchInputIndex - 1
2025-12-23 15:09:33 +08:00
if hierarchy == constants . MeasTagRecommendHierarchyType {
specificalKeyIndex = searchInputIndex - 2
}
2025-12-04 17:26:35 +08:00
if specificalKeyIndex >= 0 {
specificalKey = inputSlice [ specificalKeyIndex ]
}
2025-12-23 16:44:31 +08:00
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 )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-23 16:44:31 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
}
if ! keyExists {
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-23 16:44:31 +08:00
RecommendType : hierarchy ,
QueryDatas : [ ] string { } ,
IsFuzzy : false ,
Err : nil ,
}
}
}
2025-12-15 16:49:38 +08:00
members , err := queryMemberFromSpecificsLevel ( ctx , rdb , hierarchy , specificalKey )
2025-12-16 16:34:19 +08:00
if err != nil && err != redis . Nil {
2025-12-15 16:49:38 +08:00
logger . Error ( ctx , "query members from redis by special key failed" , "key" , specificalKey , "member" , searchInput , "op" , "SMember" , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
2025-12-04 17:26:35 +08:00
}
2025-12-15 16:49:38 +08:00
2025-12-22 10:45:47 +08:00
recommandResults := combineQueryResultByInput ( hierarchy , recommendLenType , inputSlice , members )
2026-07-09 16:31:19 +08:00
if hierarchy == constants . MeasTagRecommendHierarchyType {
recommandResults = confirmMeasRecommendResultsByHash ( ctx , rdb , inputSlice , members , recommandResults )
}
if hierarchy == constants . ConfigRecommendHierarchyType {
2025-12-29 15:58:59 +08:00
// 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
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : recommandResults ,
IsFuzzy : false ,
Err : nil ,
}
2025-12-04 17:26:35 +08:00
}
2025-12-15 16:49:38 +08:00
keyExists , err := rdb . SIsMember ( ctx , redisSetKey , searchInput ) . Result ( )
2025-12-04 17:26:35 +08:00
if err != nil {
2025-12-15 16:49:38 +08:00
logger . Error ( ctx , "check key exist from redis set failed " , "key" , redisSetKey , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
2025-12-04 17:26:35 +08:00
}
if keyExists {
2025-12-15 16:49:38 +08:00
var QueryData [ ] string
if hierarchy == constants . MaxIdentifyHierarchy || ( hierarchy == constants . IdentifyHierarchy && redisSetKey == constants . RedisAllMeasTagSetKey ) {
QueryData = [ ] string { "" }
} else {
QueryData = [ ] string { "." }
}
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : QueryData ,
IsFuzzy : false ,
Err : nil ,
2025-12-06 18:32:00 +08:00
}
2025-12-04 17:26:35 +08:00
}
// start redis fuzzy search
2026-06-29 14:13:58 +08:00
recommends , offset , err := runFuzzySearch ( ctx , rdb , searchInput , searchPrefix , hierarchy , recommendLenType )
2025-12-04 17:26:35 +08:00
if err != nil {
2025-12-06 18:32:00 +08:00
logger . Error ( ctx , "fuzzy search failed by hierarchy" , "hierarchy" , hierarchy , "search_input" , searchInput , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : false ,
Err : err ,
}
2025-12-04 17:26:35 +08:00
}
2026-06-30 13:55:42 +08:00
isFallback := false
2025-12-04 17:26:35 +08:00
if len ( recommends ) == 0 {
2025-12-22 10:45:47 +08:00
logger . Info ( ctx , "fuzzy search without result" , "hierarchy" , hierarchy , "search_input" , searchInput , "error" , err )
2026-07-07 17:11:25 +08:00
recommends , err = fallbackCurrentLevelByInput ( ctx , rdb , hierarchy , inputSlice )
2026-06-30 13:55:42 +08:00
if err != nil {
logger . Error ( ctx , "fallback all keys at current level failed" , "hierarchy" , hierarchy , "search_input" , searchInput , "error" , err )
2026-07-08 14:41:13 +08:00
return SearchResult {
2026-06-30 13:55:42 +08:00
RecommendType : hierarchy ,
QueryDatas : nil ,
IsFuzzy : true ,
IsFallback : true ,
Err : err ,
}
}
isFallback = true
2026-07-09 16:31:19 +08:00
if _ , ok := fallbackSpecificSetKey ( hierarchy , inputSlice ) ; ok {
recommends = combineQueryResultByInput ( hierarchy , recommendLenType , inputSlice , recommends )
offset = recommendPrefixOffset ( inputSlice [ : len ( inputSlice ) - 1 ] )
} else {
offset = 0
}
2025-12-04 17:26:35 +08:00
}
2025-12-12 14:19:50 +08:00
2026-07-08 14:41:13 +08:00
return SearchResult {
2025-12-15 16:49:38 +08:00
RecommendType : hierarchy ,
QueryDatas : recommends ,
IsFuzzy : true ,
2026-06-30 13:55:42 +08:00
IsFallback : isFallback ,
2026-06-29 14:13:58 +08:00
Offset : offset ,
2025-12-15 16:49:38 +08:00
Err : nil ,
}
2025-12-04 17:26:35 +08:00
}
2026-07-07 17:11:25 +08:00
// runFuzzySearch 执行 RediSearch 模糊搜索, 并用 Redis set 校验候选项确属当前层级。
2026-06-29 14:13:58 +08:00
func runFuzzySearch ( ctx context . Context , rdb * redis . Client , searchInput string , searchPrefix string , hierarchy constants . RecommendHierarchyType , recommendLenType string ) ( [ ] string , int , error ) {
2025-12-23 14:52:39 +08:00
var configToken string
var comparePrefix string
2026-07-09 16:31:19 +08:00
var memberComparePrefix string
2026-06-30 13:55:42 +08:00
searchInputLen := len ( [ ] rune ( searchInput ) )
2025-12-23 14:52:39 +08:00
compareHierarchyLen := int ( hierarchy )
comparePrefix = searchPrefix
2026-07-09 16:31:19 +08:00
memberComparePrefix = searchPrefix
2025-12-23 14:52:39 +08:00
if hierarchy == constants . MeasTagRecommendHierarchyType {
lastDotIndex := strings . LastIndex ( searchPrefix , "." )
if lastDotIndex == - 1 {
configToken = ""
} else {
configToken = searchPrefix [ lastDotIndex + 1 : ]
2026-07-09 16:31:19 +08:00
memberComparePrefix = searchPrefix [ : lastDotIndex ]
2025-12-23 14:52:39 +08:00
}
}
2025-12-23 16:44:31 +08:00
2025-12-04 17:26:35 +08:00
for searchInputLen != 0 {
2025-12-31 16:24:27 +08:00
fuzzyInput := searchInput
if comparePrefix != "" {
fuzzyInput = comparePrefix + "." + searchInput
}
2025-12-23 14:52:39 +08:00
results , err := ac . SuggestOpts ( fuzzyInput , redisearch . SuggestOptions {
2025-12-04 17:26:35 +08:00
Num : math . MaxInt16 ,
Fuzzy : true ,
WithScores : false ,
WithPayloads : false ,
} )
if err != nil {
2025-12-23 14:52:39 +08:00
logger . Error ( ctx , "query key by redis fuzzy search failed" , "query_key" , fuzzyInput , "error" , err )
2026-06-29 14:13:58 +08:00
return nil , 0 , fmt . Errorf ( "redisearch suggest failed: %w" , err )
2025-12-04 17:26:35 +08:00
}
if len ( results ) == 0 {
2026-06-30 13:55:42 +08:00
// 如果没有结果,退一步(删除最后一个字符)并继续循环
// TODO 考虑使用其他方式代替 for 循环退一字符的查询方式
searchInput = trimLastRune ( searchInput )
searchInputLen = len ( [ ] rune ( searchInput ) )
2025-12-04 17:26:35 +08:00
continue
}
var recommends [ ] string
for _ , result := range results {
2025-12-06 18:32:00 +08:00
term := result . Term
2025-12-23 14:52:39 +08:00
var termHierarchyLen int
2025-12-06 18:32:00 +08:00
var termPrefix string
2025-12-23 14:52:39 +08:00
var termLastPart string
2025-12-06 18:32:00 +08:00
lastDotIndex := strings . LastIndex ( term , "." )
if lastDotIndex == - 1 {
termPrefix = ""
2025-12-23 14:52:39 +08:00
termLastPart = term
2025-12-06 18:32:00 +08:00
} else {
termPrefix = term [ : lastDotIndex ]
2025-12-23 14:52:39 +08:00
termLastPart = term [ lastDotIndex + 1 : ]
2025-12-06 18:32:00 +08:00
}
2026-06-29 14:13:58 +08:00
switch recommendLenType {
case constants . FullRecommendLength :
2025-12-31 16:24:27 +08:00
if result . Term == "" {
termHierarchyLen = 1
} else {
termHierarchyLen = strings . Count ( result . Term , "." ) + 1
}
2026-06-29 14:13:58 +08:00
case constants . IsLocalRecommendLength :
2025-12-31 16:24:27 +08:00
if result . Term == "" {
termHierarchyLen = 4
} else {
termHierarchyLen = strings . Count ( result . Term , "." ) + 4
}
2025-12-06 18:32:00 +08:00
}
2025-12-23 14:52:39 +08:00
if termHierarchyLen == compareHierarchyLen && termPrefix == comparePrefix && strings . HasPrefix ( termLastPart , searchInput ) {
2026-07-09 16:31:19 +08:00
exists , err := fuzzyRecommendMemberExists ( ctx , rdb , hierarchy , memberComparePrefix , termLastPart )
2026-07-07 17:11:25 +08:00
if err != nil || ! exists {
logger . Info ( ctx , "fuzzy recommend term not found in specific redis set" ,
"hierarchy" , hierarchy ,
"recommend_len_type" , recommendLenType ,
"query_key" , fuzzyInput ,
"term" , term ,
"term_last_part" , termLastPart ,
"exists" , exists ,
"error" , err ,
)
continue
}
2025-12-23 14:52:39 +08:00
recommend := result . Term
2026-07-09 16:31:19 +08:00
if hierarchy == constants . MeasTagRecommendHierarchyType && configToken != "" && termPrefix == memberComparePrefix {
2025-12-23 14:52:39 +08:00
recommend = strings . Join ( [ ] string { termPrefix , configToken , termLastPart } , "." )
}
recommends = append ( recommends , recommend )
2025-12-04 17:26:35 +08:00
}
}
2025-12-29 15:58:59 +08:00
2026-06-29 14:13:58 +08:00
if len ( recommends ) == 0 {
2026-06-30 13:55:42 +08:00
searchInput = trimLastRune ( searchInput )
searchInputLen = len ( [ ] rune ( searchInput ) )
2026-06-29 14:13:58 +08:00
continue
}
2026-07-07 17:11:25 +08:00
return recommends , fuzzyRecommendOffset ( searchPrefix , searchInput ) , nil
2025-12-04 17:26:35 +08:00
}
2026-06-29 14:13:58 +08:00
return [ ] string { } , 0 , nil
2025-12-04 17:26:35 +08:00
}
2026-06-30 13:55:42 +08:00
2026-07-07 17:11:25 +08:00
// fuzzyRecommendOffset 返回模糊命中的裁剪位置, 保留完整输入前缀参与 offset。
func fuzzyRecommendOffset ( searchPrefix string , searchInput string ) int {
if searchPrefix == "" {
return len ( [ ] rune ( searchInput ) )
}
return len ( [ ] rune ( searchPrefix ) ) + 1 + len ( [ ] rune ( searchInput ) )
}
// fuzzyRecommendMemberExists 校验模糊候选末级 token 是否存在于对应 Redis set。
2026-07-08 14:41:13 +08:00
func fuzzyRecommendMemberExists ( ctx context . Context , rdb * redis . Client , hierarchy constants . RecommendHierarchyType , comparePrefix string , termLastPart string ) ( bool , error ) {
setKey , ok := fuzzyRecommendMemberSetKey ( hierarchy , comparePrefix )
2026-07-07 17:11:25 +08:00
if ! ok {
return true , nil
}
return rdb . SIsMember ( ctx , setKey , termLastPart ) . Result ( )
}
// fuzzyRecommendMemberSetKey 返回模糊候选校验时应使用的 Redis set key。
2026-07-08 14:41:13 +08:00
func fuzzyRecommendMemberSetKey ( hierarchy constants . RecommendHierarchyType , comparePrefix string ) ( string , bool ) {
2026-07-07 17:11:25 +08:00
switch hierarchy {
case constants . GridRecommendHierarchyType :
return constants . RedisAllGridSetKey , true
case constants . ZoneRecommendHierarchyType :
return fmt . Sprintf ( constants . RedisSpecGridZoneSetKey , comparePrefix ) , comparePrefix != ""
case constants . StationRecommendHierarchyType :
return fmt . Sprintf ( constants . RedisSpecZoneStationSetKey , lastRecommendPrefixToken ( comparePrefix ) ) , comparePrefix != ""
case constants . CompNSPathRecommendHierarchyType :
return fmt . Sprintf ( constants . RedisSpecStationCompNSPATHSetKey , lastRecommendPrefixToken ( comparePrefix ) ) , comparePrefix != ""
case constants . CompTagRecommendHierarchyType :
return fmt . Sprintf ( constants . RedisSpecCompNSPathCompTagSetKey , lastRecommendPrefixToken ( comparePrefix ) ) , comparePrefix != ""
case constants . ConfigRecommendHierarchyType :
return constants . RedisAllConfigSetKey , true
case constants . MeasTagRecommendHierarchyType :
return fmt . Sprintf ( constants . RedisSpecCompTagMeasSetKey , lastRecommendPrefixToken ( comparePrefix ) ) , comparePrefix != ""
default :
return "" , false
}
}
// lastRecommendPrefixToken 取推荐前缀中的最后一个层级 token。
func lastRecommendPrefixToken ( prefix string ) string {
if lastDotIndex := strings . LastIndex ( prefix , "." ) ; lastDotIndex != - 1 {
return prefix [ lastDotIndex + 1 : ]
}
return prefix
}
// trimLastRune 删除字符串最后一个 rune, 用于模糊搜索逐字回退。
2026-06-30 13:55:42 +08:00
func trimLastRune ( s string ) string {
runes := [ ] rune ( s )
if len ( runes ) == 0 {
return s
}
return string ( runes [ : len ( runes ) - 1 ] )
}