RediSearch 开发实战

摘要

RediSearch 命令

  • 为了展示命令的使用方法,这里以JSON文档进行索引,初始化数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JSON.SET user:10001 $ '{"name":"Alice Bob","age":28,"vip":"yes","orders":[{"amount":199.99,"status":"PAID"},{"amount":59.9,"status":"CREATED"}],"comment": "I have a phone","items":["SpringCloud技术指南","Shell脚本基础"]}'

JSON.SET user:10002 $ '{"name":"Bob Frank","age":35,"vip":"no","orders":[{"amount":899.0,"status":"PAID"}],"comment": "I have a iphone","items":["Linux必知必会","Java多线程详解"]}'

JSON.SET user:10003 $ '{"name":"Carol","age":22,"vip":"no","orders":[{"amount":19.9,"status":"CANCELLED"}],"comment": "I have a mobile","items":["MySQL从删库到跑路","Oracle开发实践"]}'

JSON.SET user:10004 $ '{"name":"David Bob","age":41,"vip":"yes","orders":[{"amount":1200,"status":"PAID"},{"amount":300,"status":"PAID"}],"comment": "I have a car","items":["Spring技术指南","RocketMQ由浅入深"]}'

JSON.SET user:10005 $ '{"name":"Eve","age":30,"vip":"no","orders":[{"amount":88.8,"status":"CREATED"}],"comment": "I have a pencil","items":["Kafka从零开始","Java由浅入深"]}'

JSON.SET user:10006 $ '{"name":"Frank","age":27,"vip":"no","orders":[{"amount":499,"status":"PAID"},{"amount":129,"status":"CREATED"}],"comment": "I have a phone","items":["Redis开发实战","MongoDB从入门到实战"]}'

JSON.SET user:10007 $ '{"name":"Grace","age":33,"vip":"yes","orders":[{"amount":999.9,"status":"PAID"}],"comment": "I have a book","items":["Android开发手册","Gradle从零开始"]}'

JSON.SET user:10008 $ '{"name":"Henry","age":45,"vip":"no","orders":[{"amount":59.9,"status":"CANCELLED"}],"comment": "I have a macbook","items":["SpringBoot技术指南","Maven由浅入深"]}'

JSON.SET user:10009 $ '{"name":"Ivy","age":26,"vip":"yes","orders":[{"amount":299,"status":"PAID"},{"amount":199,"status":"PAID"}],"comment": "I have a watch","items":["Spring融会贵通","Java技术开发指南"]}'

JSON.SET user:10010 $ '{"name":"Jack","age":38,"vip":"no","orders":[{"amount":150,"status":"CREATED"}],"comment": "I have a apple","items":["Spring技术指南","Java由浅入深"]}'

  • SpringBoot暂时没有支持 RediSearch ,你可以编写Lua脚本来实现相应的功能,另外 Redisson已经提供了对 RediSearch 的支持,下面结合命令给出代码示例。

1
2
3
4
5
6
<!-- 引入 Redisson ,这里要注意,现在最新版是 4.0.0,需要 springboot 4.x -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.52.0</version>
</dependency>
  • 通用代码

1
2
3
4
@Autowired
private RedissonClient redissonClient;

RSearch rSearch = redissonClient.getSearch(StringCodec.INSTANCE);

一、索引生命周期管理类

命令 作用 核心参数 示例
FT.CREATE 创建索引 索引名、ON、PREFIX、SCHEMA 见下
FT.ALTER 给已有索引新增字段 索引名、SCHEMA ADD 见下
FT.DROPINDEX 删除索引 索引名、DD 见下
FT.INFO 查看索引信息 索引名 见下
FT._LIST 列出所有索引 见下

1️⃣ FT.CREATE

  • FT.CREATE 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FT.CREATE index_name
[ON HASH | JSON]
[PREFIX count prefix ...]
[FILTER filter]
[LANGUAGE default_lang]
[LANGUAGE_FIELD lang_attribute]
[SCORE default_score]
[SCORE_FIELD score_attribute]
[PAYLOAD_FIELD payload_attribute]
[MAXTEXTFIELDS]
[TEMPORARY seconds]
[NOOFFSETS] [NOHL] [NOFIELDS] [NOFREQS]
[STOPWORDS count stopword ...]
[SKIPINITIALSCAN]
SCHEMA
field_name [AS alias] TEXT | TAG | NUMERIC | GEO | VECTOR
[SORTABLE [UNF]]
[NOINDEX]
  • FT.CREATE 参数说明表

参数 作用说明
index_name 索引名称
ON HASH | JSON 指定索引数据来源类型,默认 HASH
PREFIX count prefix... 指定索引 Key 前缀
FILTER filter 对索引数据设置过滤表达式
LANGUAGE 指定默认分词语言(默认 english,中文是 chinese)
LANGUAGE_FIELD 指定文档中的语言字段
SCORE 设置文档默认评分
SCORE_FIELD 从字段中读取评分
PAYLOAD_FIELD 指定存储的二进制负载字段
MAXTEXTFIELDS 允许更多 TEXT 字段(消耗更多内存)
TEMPORARY seconds 创建临时索引,超时后自动删除
NOOFFSETS 不存储文本偏移量(节省内存)
NOHL 禁用高亮
NOFIELDS 不保存字段内容
NOFREQS 不保存词频信息
STOPWORDS count ... 指定停用词
SKIPINITIALSCAN 创建索引时不扫描已有数据
SCHEMA 索引字段定义起始
AS alias 字段别名
TEXT 全文索引字段
TAG 精确匹配字段
NUMERIC 数值字段
GEO 地理位置字段
VECTOR 向量字段
SORTABLE 允许排序
UNF 不规范化排序
NOINDEX 字段不参与索引
  • FT.CREATE 的核心语法

1
2
3
4
5
6
FT.CREATE <index>
[ON HASH | JSON]
[PREFIX count prefix ...]
[LANGUAGE default_lang]
SCHEMA
field_name [AS alias] TEXT | TAG | NUMERIC | GEO | VECTOR
  • 创建索引示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FT.CREATE idx:user
ON JSON
PREFIX 1 user:
LANGUAGE chinese
SCHEMA
$.name AS name TEXT
$.age AS age NUMERIC SORTABLE
$.orders[*].amount AS amount NUMERIC
$.orders[*].status AS status TAG
$.comment AS comment TEXT
$.items[*] AS items TEXT

# 参数说明
# idx:user:索引名
# ON JSON:索引 JSON 文档,RediSearch 仅支持对 HASH 和 JSON 进行索引
# PREFIX 1 user: :索引指定 key 前缀,1 表示索引一个,user: 表示索引的 key 前缀,如果索引两个:PREFIX 2 user: order:
# LANGUAGE chinese:指定默认分词语言,默认是英文 english,中文是 chinese,如果索引中有中文,则必须指定 LANGUAGE 为 chinese
# SCHEMA:索引字段
# $.name AS name TEXT:索引字段 $.name(注意:JSON类型以 $. 开头,Hash类型就不需要了),字段别名为 name,字段类型为 TEXT
# AS:字段别名(查询时使用)
# TEXT / NUMERIC / TAG:字段类型,这几个是最常用的
# SORTABLE:NUMERIC、TAG、TEXT 或 GEO 类型的字段可以带有一个可选的 SORTABLE 参数,表示为字段建立专门的排序数据结构(类似列式存储),从而避免在查询阶段对大量结果进行临时排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
rSearch.createIndex(
"idx:user",
IndexOptions.defaults()
.on(IndexType.JSON)
.prefix(List.of("user:"))
.language("chinese"),
FieldIndex.text("$.name").as("name"),
FieldIndex.numeric("$.age").as("age"),
FieldIndex.numeric("$.orders[*].amount").as("amount"),
FieldIndex.tag("$.orders[*].status").as("status"),
FieldIndex.text("$.comment").as("comment"),
FieldIndex.text("$.items[*]").as("items")
);
  • 索引字段的类型

字段类型 中文名称 功能说明 典型使用场景 备注 / 限制
TEXT 全文文本字段 支持对字段值进行全文检索(分词、相关度计算、模糊匹配等) 文章内容、用户名、描述信息 支持权重(WEIGHT)、排序(SORTABLE)等选项
TAG 标签字段 / 精确匹配字段 支持精确匹配查询,适用于枚举值或离散分类 分类、状态、主键、类型字段 不分词;可自定义分隔符
NUMERIC 数值字段 支持数值范围查询 年龄、价格、分数、时间戳 支持区间查询([min max]
GEO 地理位置字段(点) 支持以“点”为中心的半径范围查询 门店位置、用户位置 值格式必须为 "经度,纬度"
VECTOR 向量字段 支持向量相似度搜索(KNN 等) 语义搜索、推荐系统、Embedding 向量 需要 Query Dialect ≥ 2(RediSearch ≥ 2.4)
GEOSHAPE 地理形状字段(多边形) 支持多边形空间查询 行政区、商圈、地图区域 使用 WKT 格式;不支持 JSON 多值和 SORTABLE

GEOSHAPE 字段补充说明

项目 说明
数据格式 WKT(Well-Known Text)格式,如:POLYGON((x1 y1, x2 y2, ...))
坐标系统 SPHERICAL(球面坐标,经纬度)
FLAT(平面坐标,笛卡尔 X/Y)
默认坐标系统 SPHERICAL
当前限制 ❌ 不支持 JSON 多值
❌ 不支持 SORTABLE 选项

索引字段类型选型建议

使用场景 推荐字段类型 说明
全文检索 TEXT 支持分词、相关度计算、模糊匹配等全文搜索能力
精确过滤 / 分类 TAG 精确匹配,不分词,适合枚举值、状态、类型等字段
区间筛选 / 排序 NUMERIC 支持数值区间查询与排序,适合价格、时间、分数等
附近的人 / 门店 GEO 基于经纬度点进行半径范围查询
语义搜索 / 向量召回 VECTOR 基于向量相似度(KNN)的语义搜索,需要 Dialect ≥ 2
复杂地理区域判断 GEOSHAPE 支持多边形(Polygon)区域查询,适合行政区、商圈等

哪些字段适合使用 SORTABLE

字段类型 是否适合 SORTABLE 原因
NUMERIC ✅ 强烈推荐 数值排序最常见,如价格、时间、评分
TAG ⚠️ 视情况 一般用于过滤,排序意义不大
TEXT ⚠️ 谨慎 文本可能很大,内存开销高
GEO ⚠️ 特殊场景 通常按距离排序,更多依赖 GEOFILTER

小贴士:如何在redis终端输入多行的命令?

  • 先说结论:redis终端 不支持多行输入,但可以通过如下方式输入多行命令:
1
2
3
4
5
6
redis-cli <<EOF
HSET user:1 \
name Tom \
age 18 \
city Beijing
EOF
  • 这种方法是利用了 Bash 的能力,即 <<EOF 后面的内容会作为标准输入,直到 EOF 为止,并通过 \ 符换行。
  • 如果在VSCode中,可以选中要合并的多行内容,然后通过 Ctrl + Shift + J 快捷键将多行合并到一行。

2️⃣ FT.ALTER

⚠️ 只能 新增字段,不能修改或删除已有字段

1
FT.ALTER idx:user SCHEMA ADD $.vip AS vip TAG
1
2
3
4
5
rSearch.alter(
"idx:user",
false,
FieldIndex.tag("$.vip").as("vip")
);

3️⃣ FT.DROPINDEX

⚠️ 删除索引

1
2
3
FT.DROPINDEX idx:user
# ⚠️ DD:删除索引的同时删除被索引数据,谨慎使用
FT.DROPINDEX idx:user DD
1
2
rSearch.dropIndex("idx:user");
rSearch.dropIndexAndDocuments("idx:user");

4️⃣ FT.INFO

查看索引信息

1
FT.INFO idx:user
1
IndexInfo info = rSearch.info("idx:user");

重点关注字段

字段 含义 解释
num_docs 当前被索引、可被搜索的文档数量 user:10001 ~ user:10010,10条文档
num_terms 词典中唯一词项 term 的数量
主要来源于 TEXT(name) 和 TAG(vip, status) 字段去重之后的数量
name: alice, bob, carol, …
vip: yes, no
status: PAID, CREATED, CANCELLED
num_records 倒排索引中的记录条目总数
所有 term 在所有文档中的出现次数之和
JSON 数组字段 orders[],每个 order 都会生成独立的索引 entry
多值字段 × 多文档 = record 爆炸式增长

num_records 真正影响:内存、查询速度、聚合成本

5️⃣ FT._LIST

列出所有索引

1
FT._LIST
1
List<String> indexes = rSearch.getIndexes();

二、查询类(核心)

命令 作用 适用场景
FT.SEARCH 标准搜索 90% 场景
FT.AGGREGATE 分组 / 统计 / 聚合 报表、分析
FT.HYBRID 文本 + 向量混合 向量搜索
FT.EXPLAIN 查询执行计划 调优
FT.EXPLAINCLI CLI 可读执行计划 调试
FT.PROFILE 性能分析 慢查询
  • FT.SEARCH 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FT.SEARCH index query
[NOCOENTENT] [VERBATIM] [NOSTOPWORDS]
[WITHSCORES] [WITHPAYLOADS] [WITHSORTKEYS]
[FILTER numeric_field min max ...]
[GEOFILTER geo_field lon lat radius m|km|mi|ft]
[INKEYS count key ...]
[INFIELDS count field ...]
[RETURN count identifier [AS property] ...]
[SUMMARIZE FIELDS count field ... FRAGS num LEN fragsize SEPARATOR sep]
[HIGHLIGHT FIELDS count field ... TAGS open close]
[SLOP slop] [TIMEOUT timeout] [INORDER]
[LANGUAGE language]
[EXPANDER expander]
[SCORER scorer]
[EXPLAINSCORE]
[PAYLOAD payload]
[SORTBY sortby ASC|DESC]
[LIMIT offset num]
[PARAMS nargs name value ...]
[DIALECT dialect]
  • FT.SEARCH 参数说明表

参数 作用说明
index 索引名称
query 查询条件(类似 SQL WHERE)
NOCONTENT 仅返回文档 ID,不返回内容
VERBATIM 禁用查询优化,完全按原查询执行
NOSTOPWORDS 查询时不忽略停用词
WITHSCORES 返回匹配文档的相关性评分
WITHPAYLOADS 返回文档的 payload 数据
WITHSORTKEYS 返回排序使用的 key
FILTER 数值字段过滤(范围查询)
GEOFILTER 地理位置范围查询
INKEYS 仅在指定的 key 集合中搜索
INFIELDS 仅在指定字段中搜索
RETURN 指定返回的字段
SUMMARIZE 返回字段内容摘要
HIGHLIGHT 高亮匹配的关键词
SLOP 设置短语查询中允许的词距
TIMEOUT 设置查询超时时间
INORDER 短语必须按顺序匹配
LANGUAGE 指定查询语言
EXPANDER 使用自定义查询扩展器
SCORER 使用自定义评分函数
EXPLAINSCORE 返回评分计算详情
PAYLOAD 给评分函数传入自定义参数
SORTBY 按指定字段排序
ASC / DESC 排序方向
LIMIT 分页控制
PARAMS 参数化查询
DIALECT 指定查询语法版本
  • FT.SEARCH 核心语法

1
2
3
4
5
FT.SEARCH index
query
[RETURN count identifier [AS property] ...]
[SORTBY sortby ASC|DESC]
[LIMIT offset num]

标准搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FT.SEARCH idx:user
'@status:{PAID} @amount:[50 +inf]'
RETURN 3 name status amount
SORTBY amount DESC
LIMIT 0 10
## 参数说明
# 查询条件:'@status:{PAID} @amount:[50 +inf]'
# 查询条件语法:
# 'xxx': 全文检索包含 xxx 的数据,会从 TEXT 字段中匹配
# '@field:value': 匹配字段 field 的值是 value,要求 field 的类型为 TEXT,全文检索
# '@field:{xxx}':精确匹配,{xxx*}:匹配前缀,{xxx|yyy}:匹配任意一个,要求 field 的类型为 TAG
# '@field:[min max]': 范围匹配,这里 +inf 表示正无穷大,要求 field 的类型为 NUMERIC
# 多个条件空格分隔
# 模糊匹配:* 表示任意字符,即匹配所有
# RETURN 3 name status amount 返回字段,3:表示返回 3 个字段
# LIMIT 0 10 分页,0:表示从第 0 条开始,10:表示返回 10 条数据
# SORTBY amount DESC 排序,amount:表示排序字段,DESC:表示降序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SearchResult result = rSearch.search(
"idx:user",
"@status:{PAID} @amount:[50 +inf]",
QueryOptions.defaults()
.returnAttributes(new ReturnAttribute("name"), new ReturnAttribute("status"),new ReturnAttribute("amount"))
.sortBy("amount")
.sortOrder(SortOrder.DESC)
.limit(0, 10));

long total = result.getTotal();
System.out.println("total = " + total);
List<Document> docs = result.getDocuments();

for (Document doc: docs) {
String id = doc.getId();
Map<String, Object> attrs = doc.getAttributes();
System.out.println("id = " + id + " attrs = " + attrs);
}
1
2
3
4
5
6
7
8
9
10
# 全文检索,从 所有 TEXT 字段中匹配,不区分大小写,即 phone/Phone/PHONE 都匹配
FT.SEARCH idx:user 'phone'
## 示例结果
1) (integer) 2
2) "user:10006"
3) 1) "$"
2) "{\"name\":\"Frank\",\"age\":27,\"vip\":\"no\",\"orders\":[{\"amount\":499,\"status\":\"PAID\"},{\"amount\":129,\"status\":\"CREATED\"}],\"comment\":\"I have a phone\",\"items\":[\"Redis\xe5\xbc\x80\xe5\x8f\x91\xe5\xae\x9e\xe6\x88\x98\",\"MongoDB\xe4\xbb\x8e\xe5\x85\xa5\xe9\x97\xa8\xe5\x88\xb0\xe5\xae\x9e\xe6\x88\x98\"]}"
4) "user:10001"
5) 1) "$"
2) "{\"name\":\"Alice Bob\",\"age\":28,\"vip\":\"yes\",\"orders\":[{\"amount\":199.99,\"status\":\"PAID\"},{\"amount\":59.9,\"status\":\"CREATED\"}],\"comment\":\"I have a phone\",\"items\":[\"SpringCloud\xe6\x8a\x80\xe6\x9c\xaf\xe6\x8c\x87\xe5\x8d\x97\",\"Shell\xe8\x84\x9a\xe6\x9c\xac\xe5\x9f\xba\xe7\xa1\x80\"]}"
1
2
3
4
5
SearchResult result = rSearch.search(
"idx:user",
"phone",
QueryOptions.defaults()
);
1
2
3
4
5
6
7
8
9
10
# 中文检索
FT.SEARCH idx:user '开始'
## 示例结果
1) (integer) 2
2) "user:10005"
3) 1) "$"
2) "{\"name\":\"Eve\",\"age\":30,\"vip\":\"no\",\"orders\":[{\"amount\":88.8,\"status\":\"CREATED\"}],\"comment\":\"I have a pencil\",\"items\":[\"Kafka\xe4\xbb\x8e\xe9\x9b\xb6\xe5\xbc\x80\xe5\xa7\x8b\",\"Java\xe7\x94\xb1\xe6\xb5\x85\xe5\x85\xa5\xe6\xb7\xb1\"]}"
4) "user:10007"
5) 1) "$"
2) "{\"name\":\"Grace\",\"age\":33,\"vip\":\"yes\",\"orders\":[{\"amount\":999.9,\"status\":\"PAID\"}],\"comment\":\"I have a book\",\"items\":[\"Android\xe5\xbc\x80\xe5\x8f\x91\xe6\x89\x8b\xe5\x86\x8c\",\"Gradle\xe4\xbb\x8e\xe9\x9b\xb6\xe5\xbc\x80\xe5\xa7\x8b\"]}"
1
2
3
4
5
SearchResult result = rSearch.search(
"idx:user",
"开始",
QueryOptions.defaults()
);

小贴士

  • 看到中文输出是乱码,可以在登录客户端时加上 --raw
1
2
3
4
5
6
7
8
9
redis-cli --user admin -a 123456 --raw
127.0.0.1:6379> FT.SEARCH idx:user '开始'
2
user:10005
$
{"name":"Eve","age":30,"vip":"no","orders":[{"amount":88.8,"status":"CREATED"}],"comment":"I have a pencil","items":["Kafka从零开始","Java由浅入深"]}
user:10007
$
{"name":"Grace","age":33,"vip":"yes","orders":[{"amount":999.9,"status":"PAID"}],"comment":"I have a book","items":["Android开发手册","Gradle从零开始"]}
SQL WHERE 与 RediSearch 查询语法对照表
  • x/y/nameTEXT/TAG(要加上{}) 字段,numNUMERIC 字段,查询语法用于 FT.SEARCH/FT.AGGREGATE 的查询字符串部分

ID SQL 条件 RediSearch 对等写法 说明
1 WHERE x = 'book' AND y = 'phone' @x:book @y:phone AND 为默认关系(空格)
2 WHERE x = 'book' AND y != 'phone' @x:book -@y:phone - 表示 NOT
3 WHERE x = 'book' OR y = 'phone' (@x:book) | (@y:phone) OR 必须显式写 |
4 WHERE x IN ('book','phone','hello world') @x:(book|phone|"hello world") 多值 OR,短语需加引号
5 WHERE y='book' AND x NOT IN ('book','phone') @y:book -(@x:book|@x:phone) NOT IN = NOT + OR
6 WHERE x NOT IN ('book','phone') -@x:(book|phone) 整体否定
7 WHERE num BETWEEN 10 AND 20 @num:[10 20] 数值区间(闭区间)
8 WHERE num >= 10 @num:[10 +inf] +inf 表示正无穷
9 WHERE num > 10 @num:[(10 +inf] ( 表示不包含
10 WHERE num < 10 @num:[-inf (10] 小于(不含 10)
11 WHERE num <= 10 @num:[-inf 10] 小于等于
12 WHERE num < 10 OR num > 20 @num:[-inf (10] | @num:[(20 +inf] 数值 OR
13 WHERE name LIKE 'Li%' @name:Li* 前缀匹配

2️⃣ FT.AGGREGATE

分组 / 统计 / 聚合,语法

  • FT.AGGREGATE 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FT.AGGREGATE index query
[VERBATIM]
[LOAD count field ... | LOAD *]
[TIMEOUT timeout]
[GROUPBY nargs property ...
REDUCE function nargs arg ... [AS name] ...]
[SORTBY nargs property ASC|DESC ... [MAX num]]
[APPLY expression AS name]
[LIMIT offset num]
[FILTER filter]
[WITHCURSOR [COUNT read_size]]
[MAXIDLE idle_time]
[PARAMS nargs name value ...]
[DIALECT dialect]
  • FT.AGGREGATE 参数说明表

参数 作用说明
index 索引名称
query 查询条件(类似 WHERE)
VERBATIM 禁用查询优化
LOAD 加载原始字段
TIMEOUT 查询超时时间
GROUPBY 分组字段(类似 SQL GROUP BY)
REDUCE 聚合函数(COUNT / SUM / AVG / MIN / MAX 等)
AS 聚合结果别名
SORTBY 对聚合结果排序
ASC / DESC 排序方向
MAX 最大返回条数
APPLY 对结果字段进行表达式计算
LIMIT 分页控制
FILTER 结果过滤(WHERE / HAVING)
WITHCURSOR 使用游标(大结果集)
COUNT 每次读取数量
MAXIDLE 游标最大空闲时间
PARAMS 参数化查询
DIALECT 查询语法版本
  • FT.AGGREGATE 的核心语法

1
2
3
4
5
FT.AGGREGATE <index> <query>
[GROUPBY ...]
[REDUCE ...]
[SORTBY ...]
[LIMIT ...]
  • 示例 1:按 status 分组统计数量

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
FT.AGGREGATE idx:user
'*'
GROUPBY 1 @status
REDUCE COUNT 0 AS cnt
## 参数说明
# *: 查询条件,* 表示匹配索引中的 所有文档,也可以是 FT.SEARCH 中的查询条件,比如:'@status:{PAID} @amount:[50 +inf]'
# GROUPBY 1 @status: 分组字段
# 1:表示分组字段数量
# @status:表示分组字段
# REDUCE COUNT 0 AS cnt: 聚合字段
# COUNT:聚合函数:计数
# 0:参数个数(COUNT 不需要参数)
# AS cnt:表示聚合字段别名
## 返回结果示例
1) (integer) 3
2) 1) "status"
2) "CANCELLED"
3) "cnt"
4) "2"
3) 1) "status"
2) "PAID"
3) "cnt"
4) "6"
4) 1) "status"
2) "CREATED"
3) "cnt"
4) "2"

# 对应SQL 语句:
SELECT status, COUNT(*) AS cnt
FROM user
GROUP BY status;
1
2
3
4
5
6
7
8
9
10
11
12
AggregationResult result = rSearch.aggregate(
"idx:user",
"*",
AggregationOptions.defaults()
.groupBy(GroupBy.fieldNames("@status").reducers(Reducer.count().as("cnt")))
);
final long total = result.getTotal();
System.out.println("total = " + total);
final List<Map<String, Object>> attributes = result.getAttributes();
for (Map<String, Object> attribute : attributes) {
System.out.println("attribute = " + attribute);
}
  • 示例 2:按 status 分组统计金额总和

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
FT.AGGREGATE idx:user '*'
GROUPBY 1 @status
REDUCE SUM 1 @amount AS total_amount
SORTBY 2 @total_amount DESC
LIMIT 0 3

## 参数说明
# SUM:聚合函数:求和
# 1:参数个数
# @amount:要参与求和的字段
# total_amount:表示聚合字段别名
# SORTBY 2 @total_amount DESC: 排序
# 2: 表示后面跟 2 个参数(字段 + 排序方向)
# @total_amount: 表示排序字段(REDUCE 产生的别名),DESC:表示降序
# LIMIT 0 3: 分页,0:表示从第 0 条开始,3:表示返回 3 条数据

## 示例结果
1) (integer) 3
2) 1) "status"
2) "PAID"
3) "total_amount"
4) "4096.89"
3) 1) "status"
2) "CREATED"
3) "total_amount"
4) "238.8"
4) 1) "status"
2) "CANCELLED"
3) "total_amount"
4) "79.8"

# 对应SQL 语句:
SELECT status, SUM(amount) AS total_amount
FROM user
GROUP BY status
ORDER BY total_amount DESC
LIMIT 0 3;
1
2
3
4
5
6
7
8
9
10
11
12
AggregationResult result = rSearch.aggregate(
"idx:user",
"*",
AggregationOptions.defaults()
.groupBy(GroupBy.fieldNames("@status")
.reducers(
Reducer.sum("@amount").as("total_amount")
)
)
.sortBy(new SortedField("@total_amount", SortOrder.DESC))
.limit(0, 3)
);
  • 示例 3:同时输出数量 + 金额,并按金额降序,数量升序

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
FT.AGGREGATE idx:user '*'
GROUPBY 1 @status
REDUCE COUNT 0 AS cnt
REDUCE SUM 1 @amount AS total_amount
SORTBY 4 @total_amount DESC @cnt ASC

## 示例结果
1) (integer) 3
2) 1) "status"
2) "PAID"
3) "cnt"
4) "6"
5) "total_amount"
6) "4096.89"
3) 1) "status"
2) "CREATED"
3) "cnt"
4) "2"
5) "total_amount"
6) "238.8"
4) 1) "status"
2) "CANCELLED"
3) "cnt"
4) "2"
5) "total_amount"
6) "79.8"
1
2
3
4
5
6
7
8
9
10
11
12
13
AggregationResult result = rSearch.aggregate(
"idx:user",
"*",
AggregationOptions.defaults()
.groupBy(GroupBy.fieldNames("@status")
.reducers(
Reducer.count().as("cnt"),
Reducer.sum("@amount").as("total_amount")
)
)
.sortBy(new SortedField("@total_amount", SortOrder.DESC))
.sortBy(new SortedField("@cnt", SortOrder.ASC))
);
  • 示例 4:按状态分组,筛选总金额 > 200

1
2
3
4
5
6
7
FT.AGGREGATE idx:user '*'
GROUPBY 1 @status
REDUCE SUM 1 @amount AS total_amount
FILTER @total_amount > 200
SORTBY 2 @total_amount DESC
## 参数说明
# FILTER: 过滤器,作用在 GROUPBY + REDUCE 之后,语义等价于 SQL 的:HAVING SUM(amount) > 1000
1
2
3
4
5
6
7
8
9
10
11
12
AggregationResult result = rSearch.aggregate(
"idx:user",
"*",
AggregationOptions.defaults()
.groupBy(GroupBy.fieldNames("@status")
.reducers(
Reducer.sum("@amount").as("total_amount")
)
)
.filter("@total_amount > 200")
.sortBy(new SortedField("@total_amount", SortOrder.DESC))
);

语义执行顺序: SCAN → GROUPBY → REDUCE → FILTER (HAVING) → SORTBY → LIMIT

  • 常见可用 REDUCE 函数

函数 用途
COUNT 行数
SUM 求和
AVG 平均
MIN / MAX 极值
TO_LIST 收集字段

3️⃣ FT.EXPLAIN/FT.EXPLAINCLI

查询执行计划, 不会考虑分组聚合,只看查询条件,它的定位是:看“会不会用索引、怎么用索引”,而不是看“跑得慢不慢”。

1
2
3
4
5
6
7
8
9
FT.EXPLAINCLI idx:user '@status:{PAID}  @amount:[100 +inf]'
## 示例结果
1) INTERSECT {
2) TAG:@status {
3) paid
4) }
5) NUMERIC {100.000000 <= @amount <= inf}
6) }
7)

4️⃣ FT.PROFILE

性能分析,看“跑得慢不慢”。

1
2
3
4
5
6
7
8
9
10
11
12
FT.PROFILE idx:user
SEARCH
QUERY '@status:{PAID} @amount:[100 +inf]'
## 参数说明
# SEARCH: 标准查询,AGGREGATE: 聚合查询
# QUERY: 查询条件

FT.PROFILE idx:user
AGGREGATE
QUERY "@status:{PAID} @amount:[100 +inf]"
GROUPBY 1 @status
REDUCE SUM 1 @amount AS total_amount
关注指标
  • 第一优先级指标(直接决定查询是否慢)

指标 所在位置 含义 健康阈值 需要警惕 常见优化手段
Total profile time Shards → Total profile time 查询在搜索引擎层面的总耗时(不含网络) < 1 ms:优秀
1–10 ms:可接受
> 20 ms:需要优化
> 50 ms:结构性问题
减少结果集、拆分查询、优化索引字段
Iterator Type Iterators profile → Type 查询执行策略 INTERSECT / UNION SCAN / WILDCARD 增加可索引字段、避免模糊前缀
Estimated number of matches Child iterators 索引层估算的候选文档数 < 1k:理想
1k–10k:可接受
> 50k:过滤条件过宽 改用 TAG / NUMERIC / 冗余字段
Loader Time Result processors → Loader 从 Redis 加载并反序列化文档 < 30% Total time > 40% Total time RETURN 精简字段、NOCONTENT、缩小 JSON
  • 第二优先级指标(判断索引或模型是否合理)

指标 所在位置 含义 健康阈值 需要警惕 常见优化手段
Number of reading operations Iterators profile 实际扫描的倒排表条目数 ≈ Estimated matches 明显大于结果数 提高字段选择性、拆分条件
Estimated matches vs 实际结果数 Iterator + Result 索引估算精度 估算值略大于实际 估算 >> 实际(10x+) 调整 schema,避免低基数字段
Parsing time Shards → Parsing time 查询语法解析耗时 < 0.1 ms > 1 ms 减少动态拼接、简化语法
Pipeline creation time Shards → Pipeline creation 执行计划构建时间 < 0.05 ms > 0.5 ms 减少子句、避免复杂嵌套
  • 经验型“红线判断”速查表(非常实用)

现象 含义 是否必须处理
Estimated matches > 100k 索引字段选择错误 必须
Iterator Type = SCAN 全表扫描 必须
Loader > 50% 总耗时 返回数据过重 强烈建议
reading ops ≫ results 倒排效率低 建议
Total time > 20 ms 在线查询风险 必须
  • 核心执行策略型 Iterator(最重要)

Type 语义 典型触发场景 性能特征 调优结论
INTERSECT 多条件取交集(AND) @a:{x} @b:[1 10] 基于最小倒排表,效率最高 理想状态,优先使用
UNION 多条件取并集(OR) @a:{x|y} 候选集扩大,随 OR 数量线性增长 可接受,避免过多 OR
SCAN 全量扫描 字段未索引 / JSON Path 不可索引 O(N),文档越多越慢 必须消灭
WILDCARD 无过滤条件 "*"@field:* 候选集=全部文档 仅限分析,线上慎用
  • 索引访问型 Iterator(INTERSECT / UNION 的子节点)

Type 对应字段类型 示例查询 性能特征 备注
TAG TAG @status:{PAID} 哈希倒排,最快 强烈推荐
NUMERIC NUMERIC @amount:[100 +inf] 跳表范围扫描 范围越窄越快
TEXT TEXT @name:alice 分词匹配 分词数影响性能
GEO GEO @loc:[13.4 52.5 10 km] 空间索引 半径影响结果数
VECTOR VECTOR =>[KNN 10 @v $q] 向量搜索 常与 FILTER 联用
IDLIST 内部 小结果集 枚举 ID 自动优化
  • 逻辑 / 结构型 Iterator

Type 含义 触发示例 性能影响 是否常见
NOT 取反 -@status:{PAID} 依赖全集 较少
OPTIONAL 可选子句 ( @a:{x} )? 增加候选集 少见
EMPTY 无匹配 条件不可能满足 极快 偶发
PHRASE 短语匹配 "hello world" 比 TEXT 慢 低频
PREFIX 前缀匹配 abc* 可能退化 需谨慎

三、别名管理(生产必备)

命令 作用
FT.ALIASADD 添加别名
FT.ALIASDEL 删除别名
FT.ALIASUPDATE 更新别名指向
  • 一个索引可以有多个别名,一旦创建别名,所有 FT.SEARCH / FT.AGGREGATE / FT.INFO 等命令都可以直接使用 alias

1
2
3
4
5
6
7
8
9
10
# 添加别名: FT.ALIASADD alias index
FT.ALIASADD alias:user idx:user
# 更新别名指向: FT.ALIASUPDATE alias index
FT.ALIASUPDATE alias:user idx:user:v2
# 删除别名: FT.ALIASDEL alias
FT.ALIASDEL alias:user
# 判断索引名称是否为别名
FT.INFO alias:user
1) index_name
2) idx:user # 真实索引名称
1
2
3
rSearch.addAlias("alias:user", "idx:user");
rSearch.updateAlias("alias:user", "idx:user:v2");
rSearch.delAlias("alias:user");

索引别名的核心价值

    1. 索引无损重建(最典型场景)
    • RediSearch 不支持在线修改 schema,字段变更通常需要重建索引,索引重建时业务将不可用。
    • 有了别名,我们可以创建一个新的索引,创建好后将别名指向新的索引,旧索引不再使用,业务无影响。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    1. 旧索引
    FT.ALIASADD user_idx user_idx_v1

    2. 新建索引
    FT.CREATE user_idx_v2 ...

    3. 数据回填完成后,原子切换
    FT.ALIASUPDATE user_idx user_idx_v2

    4. 删除旧索引(可选)
    FT.DROPINDEX user_idx_v1
    1. 灰度 / 多版本并存
    • 不同环境(dev / staging / prod)
    • 不同 schema 版本
    1. 运维与发布规范化(强烈推荐)
    • 在生产环境中:永远不要让业务代码直接使用物理索引名
    1
    2
    索引真实名: 业务名_版本_时间戳
    索引别名 : 业务名

四、游标(大结果集)

命令 作用
FT.CURSOR READ 分批读取
FT.CURSOR DEL 删除游标
  • 游标用于聚合查询(AGGREGATE)的大结果集分页/分批拉取,典型场景包括:

1
2
3
4
GROUPBY / REDUCE 后结果集很大
需要流式处理聚合结果,避免一次性返回占用大量内存
客户端按批消费结果(类似数据库的 server-side cursor)
注意:游标主要用于 FT.AGGREGATE,普通 FT.SEARCH 不支持游标分页。
  • 通过 FT.AGGREGATE ... WITHCURSOR 创建游标,通过 FT.CURSOR READ 获取结果。

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
FT.AGGREGATE idx:user "*"
GROUPBY 1 @status
REDUCE SUM 1 @amount AS total_amount
WITHCURSOR COUNT 2 MAXIDLE 60000
## 参数说明
# WITHCURSOR : 启用游标
# COUNT 2 : 每次从游标中返回的行数(批大小)
# MAXIDLE 60000 : 游标最大空闲时间(毫秒),超时后自动销毁
## 返回结果
1) 1) (integer) 3 # GROUP BY 后,一共会产生 3 行聚合结果,这里返回的是实际查询的总行数,不受COUNT 参数限制
2) 1) "status"
2) "CANCELLED"
3) "total_amount"
4) "79.8"
3) 1) "status"
2) "PAID"
3) "total_amount"
4) "4096.89"
2) (integer) 116452546 # 游标ID

# 读取游标
FT.CURSOR READ idx:user 116452546 COUNT 2
# 参数说明
# 116452546 : 游标ID
# COUNT 2 : 每次从游标中返回的行数(可覆盖初始游标中的 COUNT)
## 返回结果
1) 1) (integer) 0 # 当前游标中“剩余可读的行数”为 0
2) 1) "status"
2) "CREATED"
3) "total_amount"
4) "238.8"
2) (integer) 0 # 0 表示没有更多结果

# 销毁游标,如果设置了 MAXIDLE 参数,则该游标在 MAXIDLE 时间内自动销毁,所以无需手工销毁
FT.CURSOR DEL idx:user 116452546
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
AggregationResult result = rSearch.aggregate(
"idx:user",
"*",
AggregationOptions.defaults()
.groupBy(GroupBy.fieldNames("@status")
.reducers(
Reducer.sum("@amount").as("total_amount")
)
)
.withCursor(2, 60000)
);
long total;
long cursorId;
List<Map<String, Object>> attributes;

total = result.getTotal();
cursorId = result.getCursorId();
System.out.println("total = " + total);
System.out.println("cursorId = " + cursorId);
attributes = result.getAttributes();
for (Map<String, Object> attribute : attributes) {
System.out.println("attribute = " + attribute);
}

while (cursorId != 0) {
System.out.println("===============================================");
// 游标读取,Redisson的readCursor方法有bug,无法继续读取游标数据
// result = rSearch.readCursor("idx:user", cursorId, 2);

// 这里游标读取可以自己编写一个基于Lua 的实现
result = readCursor("idx:user", cursorId, 2);

total = result.getTotal();
cursorId = result.getCursorId();
System.out.println("total = " + total);
System.out.println("cursorId = " + cursorId);
attributes = result.getAttributes();
for (Map<String, Object> attribute : attributes) {
System.out.println("attribute = " + attribute);
}
}

// cursorId=0 或 到期 时会自动删除游标,此处无需删除
// rSearch.delCursor("idx:user", cursorId);
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
public AggregationResult readCursor(String indexName, long cursorId, int count) {
String script = "return redis.call('FT.CURSOR', 'READ', KEYS[1], ARGV[1], 'COUNT', ARGV[2])";

List<Object> results = stringRedisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(indexName),
String.valueOf(cursorId),
String.valueOf(count)
);

cursorId = (long) results.get(1);
List<Object> lo = (List) results.get(0);
return new AggregationResult(
(long) lo.get(0),
toListOfMap(lo),
cursorId
);
}

public static List<Map<String, Object>> toListOfMap(List<Object> lo) {
List<Map<String, Object>> result = new ArrayList<>();

if (lo == null || lo.size() <= 1) {
return result;
}

// 从索引 1 开始,跳过聚合总数
for (int i = 1; i < lo.size(); i++) {
Object rowObj = lo.get(i);

if (!(rowObj instanceof List<?> row)) {
continue;
}

Map<String, Object> map = new LinkedHashMap<>();

for (int j = 0; j < row.size() - 1; j += 2) {
String key = String.valueOf(row.get(j));
Object value = row.get(j + 1);
map.put(key, value);
}

result.add(map);
}

return result;
}
  • 完整生命周期总结

1
2
3
4
5
6
7
FT.AGGREGATE ... WITHCURSOR      # 创建游标(并返回第一批 + cursorId)

FT.CURSOR READ # 读取第 N 批

FT.CURSOR READ # 继续读取

FT.CURSOR DEL(可选) # 提前释放资源
  • 游标与 OFFSET/LIMIT 的区别

对比项 游标 OFFSET / LIMIT
性能 高(流式) 大 OFFSET 性能差
适合大结果集
服务器状态
适用命令 FT.AGGREGATE FT.SEARCH

五、词典 & 同义词(搜索增强)

  • 词典(DICT)和同义词(SYNONYM)只对 TEXT / TAG 搜索生效

  • FT.AGGREGATE 无关,但与 FT.SEARCH 强相关。

命令 作用
FT.DICTADD 添加词
FT.DICTDEL 删除词
FT.DICTDUMP 查看词典
FT.SYNUPDATE 更新同义词
FT.SYNDUMP 查看同义词

词典

  • 词典作用:控制“合法搜索词”,比如将词典的内容发送给用户,让用户只能搜索这些词。

    • ⚠️ 词典本身不参与搜索匹配
    • ⚠️ 只是为搜索增强提供数据来源
1
2
3
4
5
6
# 示例:添加商品相关词
FT.DICTADD product_dict iphone mobile phone android huawei
# 查看词典内容(FT.DICTDUMP)
FT.DICTDUMP product_dict
# 删除词典中的词(FT.DICTDEL)
FT.DICTDEL product_dict iphone
1
2
3
rSearch.addDict("product_dict", "iphone", "mobile","phone","android","huawei");
List<String> productDict = rSearch.dumpDict("product_dict");
rSearch.delDict("product_dict", "iphone");

同义词

  • 同义词的作用: 让不同的搜索词 命中同一批文档

1
2
3
4
5
6
例如:
搜 iphone
搜 mobile
搜 phone

👉 命中同一批订单
  • 添加 / 更新同义词(FT.SYNUPDATE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义“手机”的同义词集合,注意同义词是绑定到 索引 的
FT.SYNUPDATE idx:user
phone_group
iphone mobile phone
## 参数说明
# phone_group: 同义词集合名称(你自己定义)
# iphone mobile phone: 添加同义词,多个词用空格隔开

# 查看同义词集合
FT.SYNDUMP idx:user
## 输出
1) "phone"
2) 1) "phone_group"
3) "iphone"
4) 1) "phone_group"
5) "mobile"
6) 1) "phone_group"
1
2
rSearch.updateSynonyms("idx:user", "phone_group", "iphone", "mobile", "phone");
Map<String, List<String>> synonyms = rSearch.dumpSynonyms("idx:user");
  • 此时我们再进行搜索,就会命中所有同义词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FT.SEARCH idx:user 'phone'
# 搜索结果
1) (integer) 4
2) "user:10001"
3) 1) "$"
2) "{\"name\":\"Alice Bob\",\"age\":28,\"vip\":\"yes\",\"orders\":[{\"amount\":199.99,\"status\":\"PAID\"},{\"amount\":59.9,\"status\":\"CREATED\"}],\"comment\":\"I have a phone\",\"items\":[\"SpringCloud\xe6\x8a\x80\xe6\x9c\xaf\xe6\x8c\x87\xe5\x8d\x97\",\"Shell\xe8\x84\x9a\xe6\x9c\xac\xe5\x9f\xba\xe7\xa1\x80\"]}"
4) "user:10006"
5) 1) "$"
2) "{\"name\":\"Frank\",\"age\":27,\"vip\":\"no\",\"orders\":[{\"amount\":499,\"status\":\"PAID\"},{\"amount\":129,\"status\":\"CREATED\"}],\"comment\":\"I have a phone\",\"items\":[\"Redis\xe5\xbc\x80\xe5\x8f\x91\xe5\xae\x9e\xe6\x88\x98\",\"MongoDB\xe4\xbb\x8e\xe5\x85\xa5\xe9\x97\xa8\xe5\x88\xb0\xe5\xae\x9e\xe6\x88\x98\"]}"
6) "user:10002"
7) 1) "$"
2) "{\"name\":\"Bob Frank\",\"age\":35,\"vip\":\"no\",\"orders\":[{\"amount\":899.0,\"status\":\"PAID\"}],\"comment\":\"I have a iphone\",\"items\":[\"Linux\xe5\xbf\x85\xe7\x9f\xa5\xe5\xbf\x85\xe4\xbc\x9a\",\"Java\xe5\xa4\x9a\xe7\xba\xbf\xe7\xa8\x8b\xe8\xaf\xa6\xe8\xa7\xa3\"]}"
8) "user:10003"
9) 1) "$"
2) "{\"name\":\"Carol\",\"age\":22,\"vip\":\"no\",\"orders\":[{\"amount\":19.9,\"status\":\"CANCELLED\"}],\"comment\":\"I have a mobile\",\"items\":[\"MySQL\xe4\xbb\x8e\xe5\x88\xa0\xe5\xba\x93\xe5\x88\xb0\xe8\xb7\x91\xe8\xb7\xaf\",\"Oracle\xe5\xbc\x80\xe5\x8f\x91\xe5\xae\x9e\xe8\xb7\xb5\"]}"
  • 词典 + 同义词的组合使用

1
2
3
4
5
1. 用户输入: 手机
2. 前端 / 服务端 → 从 product_dict 给提示
3. 搜索请求 → FT.SEARCH
4. RediSearch → 同义词扩展
5. 返回统一结果

六、FT.SPELLCHECK(拼写纠错)

  • FT.SPELLCHECK 用于对用户输入的搜索词进行拼写纠错与相似词推荐

  • 它并不会真正执行搜索,而是:
    ✅ 分析输入词是否存在于索引词典
    ✅ 如果不存在,基于编辑距离(Levenshtein)给出候选纠错词
    ✅ 支持自定义词典(Dictionary)增强效果

  • 🎯 核心目标

能力 说明
拼写纠错 iphone → ipohne
模糊推荐 用户拼错关键词
搜索前置纠错 提升召回率
自动学习索引词 自动从索引词库构建词典
  • 基本语法

1
2
3
FT.SPELLCHECK index query
[DISTANCE maxDistance]
[TERMS {INCLUDE | EXCLUDE} dictionary ...]
  • 参数说明表

参数 说明 默认值
index 索引名称 必填
query 用户输入词 必填
DISTANCE 最大编辑距离(1~4) 1
TERMS INCLUDE 仅使用指定词典 全部
TERMS EXCLUDE 排除指定词典
  • 示例

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
FT.SPELLCHECK idx:user 'iphnoe' DISTANCE 2
# 输出
1) 1) TERM # 固定标识,表示这是一个被分析的词
2) "iphnoe" # 用户输入词
3) 1) 1) "0.4" # 相似度评分,越小越相似
2) "iphone" # 推荐词,建议替换词

# 搜索多个词
FT.SPELLCHECK idx:user 'iphnoe linu' DISTANCE 2
1) 1) TERM
2) "iphnoe"
3) 1) 1) "0.4"
2) "iphone"
2) 1) TERM
2) "linu"
3) 1) 1) "0.4"
2) "linux"

# 增加编辑距离会显示更多的候选词
FT.SPELLCHECK idx:user 'phnoe' DISTANCE 3
1) 1) TERM
2) "phnoe"
3) 1) 1) "4"
2) "have"
2) 1) "0.8"
2) "phone"
3) 1) "0.4"
2) "iphone"
1
2
3
4
Map<String, Map<String, Double>> spellcheck = rSearch.spellcheck(
"idx:user",
"iphnoe linu",
SpellcheckOptions.defaults().distance(2));
  • score = 编辑距离 / max(len(用户输入词), len(候选词)),但并不是总是这样

输入 候选 编辑距离 maxLen score
phnoe iphone 2 6 2/6 = 0.333 ≈ 0.4
phnoe phone 1 5 0.2 → ~0.8(内部权重可能反转或调整)
phnoe have “4” → 表示 phnoe 与 have 的编辑距离是 4
1
2
3
4
5
6
7
8
9
10
11
phnoe → have
p → h (1)
h → a (2)
n → v (3)
o → e (4)
e → (删除) (5) (不同算法可能计为 4 或 5)
该词明显是低质量候选,只是 DISTANCE=3/4 时被勉强收录。

业务侧做合理过滤
如果 score >= 2 → 直接丢弃(明显噪声)
如果 score <= 1.0 → 可信候选
  • 建议策略

场景 建议
搜索框实时提示 1
搜索提交后纠错 2
模糊容错系统 2~3
  • 词典增强(Dictionary Integration)

通过 FT.DICTADD / FT.DICTDEL 人工维护一个或多个词典,把“业务词汇”主动注入到 SpellCheck 的候选词空间中。

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
默认情况下:
SpellCheck 只能从 索引倒排词典(Inverted Index Terms) 中生成候选词。

如果某些词:
不在索引里
出现频率极低
属于专有名词
新词、品牌词
那么 SpellCheck 很可能无法正确纠错。

词典增强解决的正是这个问题。

用户输入 → Tokenizer → 拆词 →

候选源合并:
- 倒排索引词典
- 自定义词典(FT.DICTADD)

编辑距离计算

候选排序

返回纠错词

✅ SpellCheck 的候选空间被人为扩大。
1
2
3
4
5
6
7
8
9
FT.DICTADD dict:tech iphone ipad macbook airpods
# 使用指定词典纠错,白名单
FT.SPELLCHECK idx:user "macboo" TERMS INCLUDE dict:tech
1) 1) TERM
2) "macboo"
3) 1) 1) "0.4"
2) "macbook"
# 排除词典,黑名单
FT.SPELLCHECK idx:user "iphnoe" TERMS EXCLUDE dict:noise
1
2
3
4
5
6
7
Map<String, Map<String, Double>> spellcheck = rSearch.spellcheck(
"idx:user",
"macboo",
SpellcheckOptions.defaults()
.distance(2)
.includedTerms("dict:tech")
);
  • FT.SPELLCHECK 与 Suggest 的差异对比(关键)

维度 FT.SPELLCHECK FT.SUGGET
目标 拼写纠错 前缀联想
输入 完整词 前缀
返回 相似词 补全词
数据来源 索引词典 独立 Trie
是否排序 否(距离优先) 是(score)
是否模糊 编辑距离 前缀 + FUZZY
使用阶段 搜索前修正 输入过程中

已废弃命令(不建议使用)

命令 状态 说明
FT.CONFIG GET Reids8 将其标记为 Deprecated 使用 CONFIG GET search-*
FT.CONFIG SET Reids8 将其标记为 Deprecated 同上
FT.TAGVALS 已经标记为 Deprecated 改用 AGGREGATE
  • ⚠️ 这里要注意,只有 Redis 8.0.0 以上版本支持 CONFIG GET search-*

FT.CONFIG GET *CONFIG GET search-* 对应参数整理

  • 1️⃣ 索引基本 / 文档表配置

FT.CONFIG 参数 FT.CONFIG 值 CONFIG search-* 参数 CONFIG 值 说明
MINPREFIX 2 search-min-prefix 2 自动补全最小前缀长度
MINSTEMLEN 4 search-min-stem-len 4 最小词干长度
MAXDOCTABLESIZE 1000000 search-max-doctablesize 1000000 最大文档表大小
MAXSEARCHRESULTS 1000000 search-max-search-results 1000000 搜索结果限制
MAXAGGREGATERESULTS unlimited search-max-aggregate-results 2147483648 聚合结果限制
MAXEXPANSIONS 200 search-max-prefix-expansions 200 前缀扩展限制
MAXPREFIXEXPANSIONS 200 search-max-prefix-expansions 200 前缀扩展限制(兼容)
RAW_DOCID_ENCODING false search-raw-docid-encoding no 文档ID是否原始编码
UPGRADE_INDEX Upgrade config for upgrading N/A N/A 升级索引配置提示
  • 2️⃣ 查询 / 评分相关配置

FT.CONFIG 参数 FT.CONFIG 值 CONFIG search-* 参数 CONFIG 值 说明
TIMEOUT 500 search-timeout 500 查询超时(ms)
ON_TIMEOUT return search-on-timeout return 超时策略
DEFAULT_SCORER TFIDF search-default-scorer BM25STD 默认评分算法
BM25STD_TANH_FACTOR 4 search-bm25std-tanh-factor 4 BM25 调整因子
MULTI_TEXT_SLOP 100 search-multi-text-slop 100 多字段查询 slop
DEFAULT_DIALECT 1 search-default-dialect 1 查询方言
  • 3️⃣ GC / 后台索引配置

FT.CONFIG 参数 FT.CONFIG 值 CONFIG search-* 参数 CONFIG 值 说明
NOGC false search-no-gc no 禁用 GC
GC_POLICY fork search-fork-gc-policy (implicit) fork GC 策略
FORKGC_SLEEP_BEFORE_EXIT 0 search-fork-gc-sleep-before-exit 0 fork GC 退出前睡眠
FORK_GC_RUN_INTERVAL 30 search-fork-gc-run-interval 30 fork GC 执行间隔(秒)
FORK_GC_CLEAN_THRESHOLD 100 search-fork-gc-clean-threshold 100 fork GC 清理阈值
FORK_GC_RETRY_INTERVAL 5 search-fork-gc-retry-interval 5 fork GC 重试间隔
FORK_GC_CLEAN_NUMERIC_EMPTY_NODES true search-_numeric-compress no 清理空数值节点
GCSCANSIZE 100 search-gc-scan-size 100 GC 每次扫描大小
  • 4️⃣ 游标 / 线程 / 并发配置

FT.CONFIG 参数 FT.CONFIG 值 CONFIG search-* 参数 CONFIG 值 说明
CURSOR_MAX_IDLE 300000 search-cursor-max-idle 300000 游标最大空闲时间
INDEX_CURSOR_LIMIT 128 search-index-cursor-limit 128 单索引游标限制
UNION_ITERATOR_HEAP 20 search-union-iterator-heap 20 UNION 查询堆大小
NO_MEM_POOLS false search-no-mem-pools no 禁用内存池
_FREE_RESOURCE_ON_THREAD true search-_free-resource-on-thread yes 线程释放资源
search_min_operation_workers N/A search-min-operation-workers 4 最小操作线程数
search-workers N/A search-workers 0 工作线程数
search-workers-priority-bias-threshold N/A search-workers-priority-bias-threshold 1 线程优先级偏置
INDEXER_YIELD_EVERY_OPS 1000 search-indexer-yield-every-ops 1000 索引器 yield 每操作数
  • 5️⃣ 实验 / 内部 /其他

FT.CONFIG 参数 FT.CONFIG 值 CONFIG search-* 参数 CONFIG 值 说明
EXTLOAD nil search-ext-load “” 外部加载模块
FRISOINI nil search-friso-ini “” 中文分词初始化
ENABLE_UNSTABLE_FEATURES false search-enable-unstable-features no 启用实验特性
_PRINT_PROFILE_CLOCK true search-_print-profile-clock yes Profiling 开关
_NUMERIC_RANGES_PARENTS 0 search-_numeric-ranges-parents 0 数值范围父节点数
VSS_MAX_RESIZE 0 search-vss-max-resize 1024 向量索引最大重分配
PARTIAL_INDEXED_DOCS false search-partial-indexed-docs no 部分索引文档标记
_PRIORITIZE_INTERSECT_UNION_CHILDREN false search-_prioritize-intersect-union-children no 优先处理 INTERSECT/UNION 子节点
_BG_INDEX_MEM_PCT_THR 100 search-_bg-index-mem-pct-thr 100 后台索引内存阈值
_NUMERIC_COMPRESS false search-_numeric-compress no 数值压缩
_BG_INDEX_OOM_PAUSE_TIME 0 search-_bg-index-oom-pause-time 0 后台索引 OOM 暂停时间