Redis 命令及数据类型 -- AutoSuggest

摘要

AutoSuggest 搜索建议(自动补全)

  • Suggest 本质解决的问题:“当用户只输入部分前缀时,如何高性能地给出候选词,而不是全文搜索结果。”

  • Suggest 能解决什么业务问题?

业务痛点 传统方案问题 Suggest 的价值
搜索框自动补全 LIKE / 模糊查询性能差 O(logN) 级前缀匹配
热门词推荐 需要额外统计系统 内置 score 排序
拼写不准确 普通前缀无法命中 FUZZY 模糊匹配
联想提示 搜索索引过重 Suggest 独立结构,轻量
高并发提示 数据库压力大 Redis 内存级吞吐
实时更新 离线词库复杂 动态增删
  • Suggest 命令与使用场景对照表

命令 作用 典型使用场景 是否高频
FT.SUGADD 添加 / 更新补全词 构建词库、动态热词更新 ⭐⭐⭐⭐
FT.SUGGET 查询补全建议 用户输入联想提示 ⭐⭐⭐⭐⭐
FT.SUGDEL 删除补全词 词下架、清理脏数据 ⭐⭐
FT.SUGLEN 统计补全词数量 监控、容量评估
  • 场景 → 命令映射总结表(推荐收藏)

业务目标 推荐命令组合 说明
自动补全 SUGADD + SUGGET 基础能力
热词排行 SUGADD(INCR) + SUGGET(WITHSCORES) 权重驱动
拼写纠错 SUGGET(FUZZY) 容错
分类推荐 SUGADD(PAYLOAD) + SUGGET(WITHPAYLOADS) 携带业务信息
词库治理 SUGDEL + SUGLEN 运维
容量监控 SUGLEN 规模评估
  • 👉 原则:输入框提示用 Suggest,搜索结果用 FT.SEARCH。

1️⃣ FT.SUGADD —— 添加 / 更新补全词

  • 向补全词库中添加一个候选词

  • 可设置 权重、payload

  • 可用于 热词排序

  • 语法

1
2
3
FT.SUGADD key string score
[INCR]
[PAYLOAD payload]
  • 参数说明

参数 含义
key Suggestion 词库 Key,索引名称
string 补全文本
score 权重(越大越靠前)
INCR 递增,累加权重
PAYLOAD 附加元数据
  • 示例

1
2
3
4
5
6
7
8
FT.SUGADD sug:search "iphone" 100
FT.SUGADD sug:search "iphone 15" 200
FT.SUGADD sug:search "ipad" 50 PAYLOAD "category=tablet"

# 查看类型
127.0.0.1:6379> type sug:search
## 输出
trietype0

2️⃣ FT.SUGGET —— 获取补全建议

  • 根据 前缀 返回最相关的候选词

  • 支持模糊匹配

  • 可返回 payload / score

  • 可用于 搜索框输入联想、拼写纠错

  • 语法

1
2
3
4
5
FT.SUGGET key prefix
[FUZZY]
[WITHSCORES]
[WITHPAYLOADS]
[MAX num]
  • 参数说明

参数 含义
prefix 用户输入前缀
FUZZY 模糊匹配(允许拼写错误)
WITHSCORES 返回权重
WITHPAYLOADS 返回 payload
MAX 最大返回数量
  • 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FT.SUGGET sug:search "ip" MAX 5
## 输出
1) "iphone 15"
2) "iphone"
3) "ipad"

FT.SUGGET sug:search "iphne" FUZZY
## 输出
1) "iphone 15"
2) "iphone"

FT.SUGGET sug:search "ip" WITHSCORES WITHPAYLOADS
## 输出
1) "iphone 15"
2) "70.71067810058594"
3) (nil)
4) "iphone"
5) "44.72135925292969"
6) (nil)
7) "ipad"
8) "28.86751365661621"
9) "category=tablet"

3️⃣ FT.SUGDEL —— 删除补全词

  • 从补全词库中移除指定词条

  • 可用于 商品下架、敏感词移除、过期关键词清理

  • 语法

1
FT.SUGDEL key string
  • 示例

1
FT.SUGDEL sug:search "iphone"

4️⃣ FT.SUGLEN —— 查看词库规模

  • 获取当前词库的词条数量

  • 语法

1
FT.SUGLEN key
  • 示例

1
2
3
FT.SUGLEN sug:search
## 输出
(integer) 3

示例代码

  • SpringBoot 的 RedisTemplate 中没有提供对AutoSuggest的封装,需要自己封装,我这里封装了一个简易的RedisSuggestTool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package com.example.demo.redissug;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* 补全建议工具类
*/

@Component
public class RedisSuggestTool {
@Autowired
private StringRedisTemplate stringRedisTemplate;

@SuppressWarnings("unchecked")
private <T> T executeLua(String script, List<String> keys, Object... args) {
return (T) stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Object.class),
keys,
Arrays.stream(args)
.map(String::valueOf)
.toArray(String[]::new)
);
}

/**
* 添加 / 更新补全词
* <p>
* FT.SUGADD key string score
* [INCR]
* [PAYLOAD payload]
*
* @param key 索引名称
* @param value 补全文本
* @param score 分数
* @param incr 是否递增,默认为false
* @param payload 补全词的附加信息,默认为空
*/
public Long sugAdd(String key,
String value,
double score,
boolean incr,
String payload) {
String lua = """
local args = {'FT.SUGADD', KEYS[1], ARGV[1], ARGV[2]}
if ARGV[3] == '1' then table.insert(args, 'INCR') end
if ARGV[4] ~= '' then table.insert(args, 'PAYLOAD'); table.insert(args, ARGV[4]) end
return redis.call(unpack(args))
""";

return executeLua(
lua,
Collections.singletonList(key),
value,
score,
incr ? "1" : "0",
payload == null ? "" : payload
);
}

public Long sugAdd(String key,
String value,
double score) {
return sugAdd(key, value, score, false, "");
}


/**
* 查询补全建议
* <p>
* FT.SUGGET key prefix
* [FUZZY]
* [WITHSCORES]
* [WITHPAYLOADS]
* [MAX num]
*
* @param key 索引名称
* @param prefix 前缀
* @param fuzzy 是否模糊匹配,默认为false
* @param withScores 是否返回分数,默认为false
* @param withPayloads 是否返回附加信息,默认为false
* @param max 最大返回数量,默认为5
*/
public List<Suggestion> sugGet(String key,
String prefix,
boolean fuzzy,
boolean withScores,
boolean withPayloads,
int max) {
String lua = """
local args = {'FT.SUGGET', KEYS[1], ARGV[1]}
if ARGV[2] == '1' then table.insert(args, 'FUZZY') end
if ARGV[3] == '1' then table.insert(args, 'WITHSCORES') end
if ARGV[4] == '1' then table.insert(args, 'WITHPAYLOADS') end
if tonumber(ARGV[5]) > 0 then table.insert(args, 'MAX'); table.insert(args, ARGV[5]) end
return redis.call(unpack(args))
""";

List<Object> raw = executeLua(
lua,
Collections.singletonList(key),
prefix,
fuzzy ? "1" : "0",
withScores ? "1" : "0",
withPayloads ? "1" : "0",
max
);

return parseSuggestionResult(raw, withScores, withPayloads);
}

// 结果解析器
private List<Suggestion> parseSuggestionResult(List<Object> raw,
boolean withScores,
boolean withPayloads) {

if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
}

List<Suggestion> list = new ArrayList<>();

int step = 1
+ (withScores ? 1 : 0)
+ (withPayloads ? 1 : 0);

for (int i = 0; i < raw.size(); i += step) {
int idx = i;

String value = String.valueOf(raw.get(idx++));
Double score = withScores ? Double.valueOf(String.valueOf(raw.get(idx++))) : null;
String payload = withPayloads ? String.valueOf(raw.get(idx++)) : null;

list.add(new Suggestion(value, score, payload));
}

return list;
}


/**
* 删除补全词
* <p>
* FT.SUGDEL key string
*
* @param key 索引名称
* @param value 补全词
*/
public Boolean sugDel(String key, String value) {
String lua = "return redis.call('FT.SUGDEL', KEYS[1], ARGV[1])";

Object result = executeLua(
lua,
Collections.singletonList(key),
value
);

return Long.valueOf(1).equals(result);
}

/**
* 获取补全词数量
* <p>
* FT.SUGLEN key
*
* @param key 索引名称
*/
public Long sugLen(String key) {
String lua = "return redis.call('FT.SUGLEN', KEYS[1])";

return executeLua(
lua,
Collections.singletonList(key)
);
}
}


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Suggestion {
private String value;
private Double score;
private String payload;
}