🐛 fix(redis): 将 Key 搜索调整为前缀匹配

- 移除搜索 pattern 的前导通配符,避免中段 Key 误命中
- 将搜索模式和六语言文案由模糊统一为前缀
- 更新模式生成与命名空间筛选回归测试
This commit is contained in:
Syngnat
2026-07-28 09:42:12 +08:00
parent a133bf4b6a
commit 861d4ace3f
11 changed files with 29 additions and 29 deletions

View File

@@ -336,10 +336,10 @@ describe('RedisViewer tree interactions', () => {
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
expect(searchInput?.props.value).toBe('app:user');
expect(updatedSearchModeGroup.props.value).toBe('fuzzy');
expect(updatedSearchModeGroup.props.value).toBe('prefix');
expect(redisBackend.RedisScanKeys).toHaveBeenLastCalledWith(
expect.any(Object),
'*[aA][pP][pP]:[uU][sS][eE][rR]*',
'[aA][pP][pP]:[uU][sS][eE][rR]*',
'0',
600,
);

View File

@@ -255,7 +255,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const [loading, setLoading] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [searchPattern, setSearchPattern] = useState('*');
const [searchMode, setSearchMode] = useState<RedisSearchMode>('fuzzy');
const [searchMode, setSearchMode] = useState<RedisSearchMode>('prefix');
const [cursor, setCursor] = useState<string>('0');
const [hasMore, setHasMore] = useState(false);
const [loadingAllKeys, setLoadingAllKeys] = useState(false);
@@ -1098,8 +1098,8 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return;
}
setSearchMode('fuzzy');
executeSearch(groupPath, 'fuzzy');
setSearchMode('prefix');
executeSearch(groupPath, 'prefix');
}, [executeSearch]);
const stopTreeTitleEvent = (event: React.SyntheticEvent<HTMLElement>) => {
@@ -2279,13 +2279,13 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
buttonStyle="solid"
style={{ flexShrink: 0 }}
>
<Radio.Button value="fuzzy">{tr('redis_viewer.search.fuzzy')}</Radio.Button>
<Radio.Button value="prefix">{tr('redis_viewer.search.prefix')}</Radio.Button>
<Radio.Button value="exact">{tr('redis_viewer.search.exact')}</Radio.Button>
</Radio.Group>
<Search
{...noAutoCapInputProps}
style={{ flex: 1 }}
placeholder={searchMode === 'exact' ? tr('redis_viewer.placeholder.search_exact') : tr('redis_viewer.placeholder.search_fuzzy')}
placeholder={searchMode === 'exact' ? tr('redis_viewer.placeholder.search_exact') : tr('redis_viewer.placeholder.search_prefix')}
value={searchInput}
onChange={handleSearchInputChange}
onSearch={handleSearch}

View File

@@ -128,13 +128,13 @@ const RedisViewerKeyToolbar: React.FC<RedisViewerKeyToolbarProps> = ({
buttonStyle="solid"
style={{ flexShrink: 0 }}
>
<Radio.Button value="fuzzy">{tr('redis_viewer.search.fuzzy')}</Radio.Button>
<Radio.Button value="prefix">{tr('redis_viewer.search.prefix')}</Radio.Button>
<Radio.Button value="exact">{tr('redis_viewer.search.exact')}</Radio.Button>
</Radio.Group>
<Search
{...noAutoCapInputProps}
style={{ flex: 1 }}
placeholder={searchMode === 'exact' ? tr('redis_viewer.placeholder.search_exact') : tr('redis_viewer.placeholder.search_fuzzy')}
placeholder={searchMode === 'exact' ? tr('redis_viewer.placeholder.search_exact') : tr('redis_viewer.placeholder.search_prefix')}
value={searchInput}
onChange={onSearchInputChange}
onSearch={onSearch}

View File

@@ -10,24 +10,24 @@ describe('normalizeRedisSearchInput', () => {
});
});
it('wraps plain keywords with wildcard for contains matching', () => {
it('adds only a trailing wildcard for prefix matching', () => {
expect(normalizeRedisSearchInput('order')).toEqual({
keyword: 'order',
pattern: '*[oO][rR][dD][eE][rR]*',
pattern: '[oO][rR][dD][eE][rR]*',
});
});
it('builds ascii case-insensitive patterns for letter keywords', () => {
expect(normalizeRedisSearchInput('agent')).toEqual({
keyword: 'agent',
pattern: '*[aA][gG][eE][nN][tT]*',
pattern: '[aA][gG][eE][nN][tT]*',
});
});
it('escapes redis glob special characters as literals', () => {
expect(normalizeRedisSearchInput('user:*:[id]?')).toEqual({
keyword: 'user:*:[id]?',
pattern: '*[uU][sS][eE][rR]:\\*:\\[[iI][dD]\\]\\?*',
pattern: '[uU][sS][eE][rR]:\\*:\\[[iI][dD]\\]\\?*',
});
});

View File

@@ -1,7 +1,7 @@
const REDIS_GLOB_SPECIAL_CHARS = /([*?\[\]\\])/g;
const ASCII_LETTER = /^[A-Za-z]$/;
export type RedisSearchMode = 'fuzzy' | 'exact';
export type RedisSearchMode = 'prefix' | 'exact';
const escapeRedisGlobLiteral = (value: string): string => {
return value.replace(REDIS_GLOB_SPECIAL_CHARS, '\\$1');
@@ -21,7 +21,7 @@ const toCaseInsensitiveRedisGlobLiteral = (value: string): string => {
export const normalizeRedisSearchInput = (
rawValue: string,
mode: RedisSearchMode = 'fuzzy',
mode: RedisSearchMode = 'prefix',
): { keyword: string; pattern: string } => {
const keyword = String(rawValue || '').trim();
if (!keyword) {
@@ -35,11 +35,11 @@ export const normalizeRedisSearchInput = (
}
return {
keyword,
pattern: `*${toCaseInsensitiveRedisGlobLiteral(keyword)}*`,
pattern: `${toCaseInsensitiveRedisGlobLiteral(keyword)}*`,
};
};
export const normalizeRedisSearchDraftChange = (rawValue: string, mode: RedisSearchMode = 'fuzzy'): {
export const normalizeRedisSearchDraftChange = (rawValue: string, mode: RedisSearchMode = 'prefix'): {
keyword: string;
pattern: string;
shouldSearchImmediately: boolean;

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "Neuen Mitgliedswert eingeben",
"redis_viewer.placeholder.search_exact": "Vollständigen Key oder Namespace für exakte Suche eingeben",
"redis_viewer.placeholder.search_fuzzy": "Keys suchen (unscharfe Suche)",
"redis_viewer.placeholder.search_prefix": "Keys nach Präfix suchen",
"redis_viewer.placeholder.stream_id": "Zum Beispiel: * oder 1723110000000-0",
"redis_viewer.placeholder.value": "Wert",
"redis_viewer.search.exact": "Exakt",
"redis_viewer.search.fuzzy": "Unscharf",
"redis_viewer.search.prefix": "Präfix",
"redis_viewer.state.connection_not_found": "Verbindung nicht gefunden",
"redis_viewer.state.empty_selection": "Wählen Sie einen Key aus, um Details anzuzeigen",
"redis_viewer.state.import_preview_empty": "Wählen Sie eine Redis-Exportdatei aus, prüfen Sie die Vorschau und wählen Sie die zu importierenden Keys aus",

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "Enter new member value",
"redis_viewer.placeholder.search_exact": "Enter full Key or namespace for exact search",
"redis_viewer.placeholder.search_fuzzy": "Search Keys (fuzzy match)",
"redis_viewer.placeholder.search_prefix": "Search Keys by prefix",
"redis_viewer.placeholder.stream_id": "For example: * or 1723110000000-0",
"redis_viewer.placeholder.value": "Value",
"redis_viewer.search.exact": "Exact",
"redis_viewer.search.fuzzy": "Fuzzy",
"redis_viewer.search.prefix": "Prefix",
"redis_viewer.state.connection_not_found": "Connection not found",
"redis_viewer.state.empty_selection": "Select a Key to view details",
"redis_viewer.state.import_preview_empty": "Select a Redis export file to preview and choose Keys to import",

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "新しいメンバー値を入力",
"redis_viewer.placeholder.search_exact": "完全一致検索する Key または名前空間を入力",
"redis_viewer.placeholder.search_fuzzy": "Key を検索(あいまい一致)",
"redis_viewer.placeholder.search_prefix": "Key を前方一致で検索",
"redis_viewer.placeholder.stream_id": "例: * または 1723110000000-0",
"redis_viewer.placeholder.value": "値",
"redis_viewer.search.exact": "完全一致",
"redis_viewer.search.fuzzy": "あいまい",
"redis_viewer.search.prefix": "前方一致",
"redis_viewer.state.connection_not_found": "接続が見つかりません",
"redis_viewer.state.empty_selection": "詳細を表示する Key を選択してください",
"redis_viewer.state.import_preview_empty": "Redis エクスポートファイルを選択し、プレビューしてインポートする Key を選択してください",

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "Введите значение нового участника",
"redis_viewer.placeholder.search_exact": "Введите полный Key или namespace для точного поиска",
"redis_viewer.placeholder.search_fuzzy": "Поиск Keys (нечеткое совпадение)",
"redis_viewer.placeholder.search_prefix": "Поиск Keys по префиксу",
"redis_viewer.placeholder.stream_id": "Например: * или 1723110000000-0",
"redis_viewer.placeholder.value": "Значение",
"redis_viewer.search.exact": "Точно",
"redis_viewer.search.fuzzy": "Нечетко",
"redis_viewer.search.prefix": "Префикс",
"redis_viewer.state.connection_not_found": "Подключение не найдено",
"redis_viewer.state.empty_selection": "Выберите Key, чтобы просмотреть детали",
"redis_viewer.state.import_preview_empty": "Выберите файл экспорта Redis, просмотрите его и отметьте Keys для импорта",

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "输入新成员值",
"redis_viewer.placeholder.search_exact": "输入完整 Key 或命名空间进行精确搜索",
"redis_viewer.placeholder.search_fuzzy": "搜索 Key(模糊匹配)",
"redis_viewer.placeholder.search_prefix": "按前缀搜索 Key",
"redis_viewer.placeholder.stream_id": "例如:* 或 1723110000000-0",
"redis_viewer.placeholder.value": "值",
"redis_viewer.search.exact": "精确",
"redis_viewer.search.fuzzy": "模糊",
"redis_viewer.search.prefix": "前缀",
"redis_viewer.state.connection_not_found": "未找到连接",
"redis_viewer.state.empty_selection": "选择一个 Key 查看详情",
"redis_viewer.state.import_preview_empty": "请选择 Redis 导出文件,预览后勾选要导入的 Key",

View File

@@ -6972,11 +6972,11 @@
"redis_viewer.placeholder.new_key_name": "new:key:name",
"redis_viewer.placeholder.new_member_value": "輸入新成員值",
"redis_viewer.placeholder.search_exact": "輸入完整 Key 或命名空間進行精確搜尋",
"redis_viewer.placeholder.search_fuzzy": "搜尋 Key(模糊比對)",
"redis_viewer.placeholder.search_prefix": "依前綴搜尋 Key",
"redis_viewer.placeholder.stream_id": "例如:* 或 1723110000000-0",
"redis_viewer.placeholder.value": "值",
"redis_viewer.search.exact": "精確",
"redis_viewer.search.fuzzy": "模糊",
"redis_viewer.search.prefix": "前綴",
"redis_viewer.state.connection_not_found": "找不到連線",
"redis_viewer.state.empty_selection": "選取一個 Key 以查看詳細資料",
"redis_viewer.state.import_preview_empty": "請選擇 Redis 匯出檔案,預覽後勾選要匯入的 Key",