🐛 fix(redis): 修复精确搜索无法命中命名空间

- 精确搜索识别无通配符的 Redis literal pattern
- 同时查询完整 Key 与同名命名空间前缀
- 修复输入 Agent 无法显示 Agent 文件夹的问题
- 避免误匹配 AgentCapacity、AgentState 等相似前缀
- 补充 glob literal 与命名空间搜索回归测试
- 更新 Redis 精确搜索输入提示文案
This commit is contained in:
Syngnat
2026-04-27 11:31:20 +08:00
parent 3665639300
commit 1b31c54917
3 changed files with 121 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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(