mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-03 13:09:47 +08:00
🐛 fix(redis): 修复精确搜索无法命中命名空间
- 精确搜索识别无通配符的 Redis literal pattern - 同时查询完整 Key 与同名命名空间前缀 - 修复输入 Agent 无法显示 Agent 文件夹的问题 - 避免误匹配 AgentCapacity、AgentState 等相似前缀 - 补充 glob literal 与命名空间搜索回归测试 - 更新 Redis 精确搜索输入提示文案
This commit is contained in:
@@ -155,6 +155,46 @@ func (r *RedisClientImpl) toPhysicalPattern(pattern string) string {
|
||||
return prefix + normalized
|
||||
}
|
||||
|
||||
func redisGlobPatternLiteralKey(pattern string) (string, bool) {
|
||||
if pattern == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(pattern); i++ {
|
||||
char := pattern[i]
|
||||
if char == '\\' {
|
||||
if i+1 >= len(pattern) {
|
||||
return "", false
|
||||
}
|
||||
i++
|
||||
builder.WriteByte(pattern[i])
|
||||
continue
|
||||
}
|
||||
if char == '*' || char == '?' || char == '[' {
|
||||
return "", false
|
||||
}
|
||||
builder.WriteByte(char)
|
||||
}
|
||||
return builder.String(), true
|
||||
}
|
||||
|
||||
func escapeRedisGlobLiteral(value string) string {
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(value); i++ {
|
||||
char := value[i]
|
||||
if char == '*' || char == '?' || char == '[' || char == ']' || char == '\\' {
|
||||
builder.WriteByte('\\')
|
||||
}
|
||||
builder.WriteByte(char)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func redisExactSearchPattern(literalKey string) (string, string) {
|
||||
return literalKey, escapeRedisGlobLiteral(literalKey) + ":*"
|
||||
}
|
||||
|
||||
func (r *RedisClientImpl) toPhysicalKeys(keys []string) []string {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
@@ -367,6 +407,15 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
if pattern == "" {
|
||||
pattern = "*"
|
||||
}
|
||||
exactPhysicalKey := ""
|
||||
if literalKey, ok := redisGlobPatternLiteralKey(pattern); ok {
|
||||
exactKey, namespacePattern := redisExactSearchPattern(literalKey)
|
||||
exactPhysicalKey = r.toPhysicalKey(exactKey)
|
||||
if exactPhysicalKey == "" {
|
||||
return &RedisScanResult{Keys: []RedisKeyInfo{}, Cursor: "0"}, nil
|
||||
}
|
||||
pattern = namespacePattern
|
||||
}
|
||||
physicalPattern := r.toPhysicalPattern(pattern)
|
||||
|
||||
isSearchPattern := pattern != "*"
|
||||
@@ -393,6 +442,10 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
keys := make([]string, 0, int(targetCount))
|
||||
seen := make(map[string]struct{}, int(targetCount))
|
||||
var mu sync.Mutex
|
||||
if exactPhysicalKey != "" {
|
||||
keys = append(keys, exactPhysicalKey)
|
||||
seen[exactPhysicalKey] = struct{}{}
|
||||
}
|
||||
|
||||
err := r.clusterClient.ForEachMaster(ctx, func(nodeCtx context.Context, node *redis.Client) error {
|
||||
var nodeCursor uint64
|
||||
@@ -453,6 +506,10 @@ func (r *RedisClientImpl) ScanKeys(pattern string, cursor uint64, count int64) (
|
||||
|
||||
keys := make([]string, 0, int(targetCount))
|
||||
seen := make(map[string]struct{}, int(targetCount))
|
||||
if exactPhysicalKey != "" && currentCursor == 0 {
|
||||
keys = append(keys, exactPhysicalKey)
|
||||
seen[exactPhysicalKey] = struct{}{}
|
||||
}
|
||||
|
||||
for len(keys) < int(targetCount) {
|
||||
if time.Since(scanStartedAt) >= maxDuration {
|
||||
|
||||
@@ -120,6 +120,69 @@ func TestNormalizeRedisGetValueError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisGlobPatternLiteralKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
wantKey string
|
||||
wantExact bool
|
||||
}{
|
||||
{name: "plain exact key", pattern: "Agent", wantKey: "Agent", wantExact: true},
|
||||
{name: "escaped glob characters stay literal", pattern: `user:\*:\[id\]\?\\raw`, wantKey: `user:*:[id]?\raw`, wantExact: true},
|
||||
{name: "fuzzy wildcard is not exact", pattern: "*[aA][gG][eE][nN][tT]*", wantExact: false},
|
||||
{name: "unescaped suffix wildcard is not exact", pattern: "Agent*", wantExact: false},
|
||||
{name: "unescaped single character wildcard is not exact", pattern: "Agent?", wantExact: false},
|
||||
{name: "unescaped character class is not exact", pattern: "Agent[0-9]", wantExact: false},
|
||||
{name: "empty pattern is not exact", pattern: "", wantExact: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotKey, gotExact := redisGlobPatternLiteralKey(tt.pattern)
|
||||
if gotExact != tt.wantExact {
|
||||
t.Fatalf("redisGlobPatternLiteralKey(%q) exact=%v, want %v", tt.pattern, gotExact, tt.wantExact)
|
||||
}
|
||||
if gotKey != tt.wantKey {
|
||||
t.Fatalf("redisGlobPatternLiteralKey(%q) key=%q, want %q", tt.pattern, gotKey, tt.wantKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisExactSearchPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
literalKey string
|
||||
wantExactKey string
|
||||
wantNamespace string
|
||||
}{
|
||||
{
|
||||
name: "plain namespace folder",
|
||||
literalKey: "Agent",
|
||||
wantExactKey: "Agent",
|
||||
wantNamespace: "Agent:*",
|
||||
},
|
||||
{
|
||||
name: "escaped namespace keeps glob chars literal",
|
||||
literalKey: `user:*:[id]?\raw`,
|
||||
wantExactKey: `user:*:[id]?\raw`,
|
||||
wantNamespace: `user:\*:\[id\]\?\\raw:*`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExactKey, gotNamespace := redisExactSearchPattern(tt.literalKey)
|
||||
if gotExactKey != tt.wantExactKey {
|
||||
t.Fatalf("redisExactSearchPattern(%q) exactKey=%q, want %q", tt.literalKey, gotExactKey, tt.wantExactKey)
|
||||
}
|
||||
if gotNamespace != tt.wantNamespace {
|
||||
t.Fatalf("redisExactSearchPattern(%q) namespace=%q, want %q", tt.literalKey, gotNamespace, tt.wantNamespace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRedisHashEntriesWithFallbackUsesHScanWhenHGetAllForbidden(t *testing.T) {
|
||||
scanCalls := 0
|
||||
values, length, err := readRedisHashEntriesWithFallback(
|
||||
|
||||
Reference in New Issue
Block a user