Redis 原生技术栈之 RediSearch 的使用
RediSearch 概述
打个形象的比喻,如果将 RedisJSON 比作一个存储复杂结构的“抽屉”,那么 RediSearch 就是给所有抽屉装上“实时雷达” 的高性能搜索引擎。在 Redis 8.2+ 的语境下,RediSearch 不再仅仅是一个简单的关键词匹配工具,它已经演变成一个多模态搜索与索引引擎。关于在 RediSearch 和 ElasticSearch 技术选型的问题,在《RediSearch 和 RedisJSON 的编译安装》中我们已经有所介绍,不再赘述。
RediSearch 是什么
传统的 Redis 只能通过 Key 来找 Value。如果你想找“所有价格在 100 到 200 之间且描述里包含 “Linu” 的商品,原生 Redis 只能笨拙地遍历所有 Key(SCAN),这在生产环境下是灾难性的。
RediSearch 的核心逻辑: 它在 Redis 内存中维护了倒排索引(Inverted Index)。当你往 Redis 存入数据时,RediSearch 会在后台自动提取关键词并建立索引。查询时,它直接在索引中高速定位结果,响应时间通常在 微秒或毫秒级。
它能做什么
RediSearch 的强大之处在于它支持多种索引类型:
- 全文搜索 (Text Search):支持词干提取(Stemming)、模糊匹配、前缀搜索。
- 数值过滤 (Numeric Filtering):支持对数字范围的高效筛选(如价格、时间戳)。
- 地理位置搜索 (Geo-filtering):基于经纬度的半径查询。
- 聚合统计 (Aggregation):类似于 SQL 的 GROUP BY,可以对搜索结果进行实时统计、求和、分组。
- 向量搜索 (Vector Search):这是目前 AI 领域最火的功能,也是多模态搜索的基础。它可以存储和检索高维向量,用于实现 AI 语义搜索、图片相似度检索或推荐系统。
关键的概念
使用 RediSearch 通常遵循以下经典的 “三步走” 流程:
- 定义 Schema(创建索引):
- 你不需要给每个文档建索引,而是定义一个“规则”。
- 指令:FT.CREATE 告诉 Redis:“我要监听所有以 “user:” 开头的 Key,并把 JSON 里的 “name” 当作文本索引,”age” 当作数值索引。”
- 自动同步(数据写入):
- 你只需要正常使用 JSON.SET 存入数据。
- RediSearch 模块会自动检测到数据变化,实时异步(或同步)更新索引。你不需要额外写索引代码。
- 执行查询:
- 指令:FT.SEARCH 支持复杂的逻辑组合,例如 “@age:[20 30] @skills:{Linux}”。
可以先来感受一下:
1 | # 1. 创建索引 |
这里针对字段 name、age、tags,我们用到了三种字段类型:
TEXT:表示对字段分词、去停用词(如 a, the)、词干提取、转小写。适用于描述、名称、或者长文本。语法例如 “@name:Owl*” (前缀), “@desc:(hello %orl%)”。NUMERIC:表示存储为数值范围树。适用于价格、年龄、时间戳、坐标。语法例如 “@age:[20 30]” 、”@price:[-inf 100]”。TAG:表示整体存储(不分词)。虽然叫 Tag,但你可以把它看作 “精确匹配字符串”。GEO:表示地理位置,专门用于存储经纬度坐标,实现附近的人、外卖配送范围、门店搜索等。- 存储格式:字符串 “经度,纬度” (例如 “116.397,39.908”)。
- 核心功能:支持圆形范围查询。
- 查询语法:”@loc:[lon lat radius m|km|ft|mi]”。
VECTOR:这是目前最强大的类型,用于存储由机器学习模型(如 Embedding 模型)生成的浮点数数组。- 存储格式:二进制形式的浮点数向量。
- 算法支持:
FLAT(暴力搜索,高精度)或HNSW(近似最近邻,高性能)。- FLAT:暴力搜索,不建立任何复杂的导航网,搜索时直接计算查询向量与库中每一个向量的距离,然后排序。它的特点就是查所有人,绝对不会漏掉最像的那一个,但是数据量一旦上万,搜索速度会直线下降。可以用在几千条的小规模数据,或不能容忍任何近似误差的场景。
- HNSW:一种近似最近邻搜索(ANN)。它预先构建一个多层图结构,搜索时像“跳跃表”一样快速逼近目标。特点就是快,即使在百万、千万级数据中,也能在毫秒级返回结果。但是为了维护那个 “导航网”,它需要额外的内存。经常用在大规模搜索(10万+ 数据量)的场景,比如实时推荐系统、识图搜索、大模型(LLM)的知识库检索。
- 核心功能:计算向量间的余弦相似度、欧式距离。
检索查询指令
FT.SEARCH 是 RediSearch 的检索指令。FT.SEARCH 后的名称既可以是索引的真实名称,也可以是索引的别名。
逻辑组合
1 | # AND (默认就是 AND) |
数组字段
比如在示例 JSON 里,tags 是一个数组 [“admin”, “developer”]。 RediSearch 极其擅长处理这种 “多值” 字段。你可以非常轻松地筛选出 “只要包含” 其中一个标签的文档:
1 | # 只要有 developer 标签的 |
权重搜索
假设你想搜包含 “Manager” 的文档,但希望标题(title)匹配的优先级高于描述(desc)。
1 | > FT.SEARCH idx:emp "(@title:Manager) | (@desc:Manager)" |
短语匹配
短语匹配 (Exact Phrase):搜“Software Engineer”这个完整词组,而不是包含这两个词的文档。
1 | > FT.SEARCH idx:emp "@title:\"Software Engineer\"" |
音近匹配
处理拼写错误,比如搜 Owlias 但用户输错了。
1 | # 使用 % 包裹,1个 % 代表允许 1 个字母错误 |
地理位置查询
如果你的 JSON 里存了经纬度(格式为 "lon,lat"),你可以实现“附近的人”。创建索引时需指定 GEO 类型: SCHEMA $.location AS loc GEO。
1 | # 查找坐标 116.39, 39.90 半径 5 公里内的员工。 |
向量查询举例
初始化数据:
1 | # 我们创建一个简单的员工索引。 |
执行向量搜索:
假设我们要搜:“找一个技术很强的人”。我们假设 “技术强” 对应的目标坐标是 [1.0, 0.0]。
在 redis-cli 中,我们需要把 [1.0, 0.0] 转为 32 位浮点数的二进制十六进制表示。1.0 的十六进制是 0000803f,0.0的十六进制是 00000000,合并后(小端字节序):”\x00\x00\x80\x3f\x00\x00\x00\x00”。
1 | # 指令解释 |
如何自己生成二进制向量?
1 | import struct |
性能优化黄金参数
在执行 FT.SEARCH 时,记得带上这些参数以节省带宽和 CPU:
NOCONTENT: 只返回文档数量 和 文档ID,不返回 JSON 内容(当你只需要计数时极快)RETURN: 只返回指定的字段,不返回整个大 JSON。LIMIT: 分页是必须的,默认只返回前 10 条。
1 | # 只返回文档数量 和 文档ID |
解释查询语句
- FT.EXPLAIN:解释查询语句,它不执行查询,而是返回 RediSearch 是如何解析你的查询语法的(类似于 SQL 的 EXPLAIN)。
- FT.EXPLAINCLI: 在命令行中以更易读的树状结构展示查询解析路径。
1 | > FT.EXPLAINCLI idx:emp "@name:Owl* | @tags:{developer}" |
常用的精确查询的指令
在 RediSearch 中,所谓的“精确过滤”指的是跳过全文检索的分词权重和评分(Scoring),直接判断文档是否满足特定条件的查询。通常,精确过滤(尤其是基于 TAG 和 NUMERIC 的过滤)通常比全文检索(TEXT)要高效得多。
在 RediSearch 中,精确匹配主要通过 FT.SEARCH 配合特定的过滤语法来实现,或者使用专门的 FILTER 参数。
1 | # TAG 类型的精确过滤(最常用,注意 TAG 默认是大小写敏感的) |
另外需要注意,有一些查询看起来像过滤,但其实非常重:
- 模糊匹配 (
%word%):这是最慢的,因为 Redis 必须计算编辑距离,遍历大量相似的词条。 - 前缀匹配 (
word\*): 虽然比模糊匹配快,但它需要扫描所有以该前缀开头的索引项。如果前缀太短(比如a*),性能会急剧下降。 - 通配符 (
\*): 在全文检索中使用通配符会导致全索引扫描。
在实际开发中,为了追求极致性能,建议遵循以下原则:
- 能用 TAG 就不用 TEXT: 例如:订单号、用户状态、分类、性别。这些永远不应该分词。
- 先过滤后搜索: 在查询时,把精确过滤条件放在前面。RediSearch 会先通过精确条件缩小结果集,再对剩下的小规模数据进行复杂的全文搜索。例如一个比较好的例子:
@status:{active} @description:程序员 - 轻易不要使用 WITHSCORES: 如果你不需要对结果进行 “匹配度排行”,请不要手动去解析分值。精确过滤天然不带分值,这反而节省了计算资源。
聚合查询指令
如果你想知道 “公司里 20 岁以上的员工有多少个,平均年龄是多少”,你不需要把数据取回 Java 内存。 聚合命令 FT.AGGREGATE 可以在 Redis 内部就可以直接完成统计:
1 | > FT.SEARCH idx:emp "@name:*li*" |
上述聚合指令中参数前面要写数字0、1,这是 RediSearch 协议的设计,为了让解析器能快速知道后面跟着多少个参数。
索引管理指令
索引的创建
1 | # 对传统 Hash 数据建索引 |
核心参数:
ON JSON/HASH:表示数据来源,必选,默认是 HASHPREFIX:表示监听哪些 Key,建议填写,不写则监听所有 Key(性能损耗大)LANGUAGE:表示默认的语言,非必选,影响分词逻辑,中文通常用chineseFILTER:过滤表达式,非必选,例如只索引 “age > 18” 的 KeyNOOFFSETS:表示不记录词的位置(无法做短语匹配),非必选,可以节省空间TEMPORARY:表示临时索引,非必选,超过指定秒数无查询则自动销毁
索引的删除
指令为 FT.DROPINDEX idx_name [DD]。默认只删除索引,保留原始数据。加上 DD (Delete Documents) 会 连带删除 所有被索引的原始 JSON 或 Hash 数据。
1 | # 删除名称为 idx:user 的索引(不删除原始文档数据,雷达坏了,但抽屉里的东西还在) |
索引的别名及蓝绿发布
蓝绿发布是一种 “全量瞬切发布”,蓝色代表用户正在访问的稳定运行的真实索引版本,绿色代表测试完成待发布的新索引版本,别名在这里充当了指针的角色,它总是指向实际正在用的索引版本。 在实际的生产中,你要要修改 Schema,直接删了重建会让业务中断,是不妥当的。正确做法是:
- 创建版本1: FT.CREATE idx:user_v1 …
- 设置别名(alias):
FT.ALIASADD idx:user idx:user_v1 - 代码中永远调用: FT.SEARCH idx:user …
- 需要改 Schema 时:
- 创建新版:FT.CREATE idx:user_v2 …(此时后台会自动开始构建索引)
- 等到 FT.INFO 显示构建完成后,秒级切换:
FT.ALIASUPDATE idx:user idx:user_v2 - 最后删掉旧的:
FT.DROPINDEX idx:user_v1
1 | # 如果觉得没别名不满意想要换掉,还可以将别名删除 |
索引的更改
索引的修改涉及的指令是 FT.ALTER。 在使用 FT.ALTER 之前,你得了解它不能做什么,否则会浪费大量的时间:
- 它不能删除字:一旦字段进入 Schema,就无法通过 ALTER 移除。
- 不能修改字段类型:如果你想把一个 TAG 字段改为 TEXT,FT.ALTER 帮不了你,必须使用蓝绿发布(重建索引)。
- 不能修改字段名称:AS 后面的别名是固定的。
- 不能修改现有属性:例如,你不能给一个现有的 TEXT 字段追加 SORTABLE 属性。
它最常见的用法就是添加新字段。当你发现现有的 JSON 数据中多了一个维度,或者之前漏掉了某个字段的索引,可以使用 SCHEMA ADD。
1 | # 执行此命令后,RediSearch 会开始在后台扫描现有的 JSON 文档。 |
索引的监控与状态查看
1 | # 查看索引详情 |
返回字段定义、文档总数、内存消耗、索引失败计数、扫描速度等。这是排查“为什么搜不到”的第一工具。你可以重点观察这几个指标:
- num_docs: 索引里到底存了多少个 JSON 文档,比如4个。
- num_records:代表索引项的总条目数,比如16个。你可能奇怪为什么 num_docs 是 4,但 num_records 是 16 呢?这是因为这里的 1个文档里有 4 个索引字段(name, age, tags, location)。如果 4 个文档都填满了这些字段,4 x 4 = 16,这证明你的字段利用率达到了 100%。
- inverted_sz_mb:倒排索引大小(影响 TEXT 搜索速度)。
- doc_table_size_mb:文档表大小(这是最大的开销项之一,存储了内部 DocID 到 Redis Key 的映射)。
- total_index_memory_sz_mb:索引内存占用多少MB。
- vector_index_sz_mb:向量 HNSW 或 Flat 算法占用的内存,如果没有定义 VECTOR 类型的字段则为0。
- percent_indexed:1 代表 100%。存量数据已经全部处理完毕,索引已是最新状态。
- hash_indexing_failures: 是否有因为格式不对导致索引失败的文档。
1 | # 在集群模式下,执行此命令只会返回当前节点上存在的索引。 |
自定义字典
RediSearch 允许你干预分词逻辑,这在处理特定行业的专有名词时很有用。
字典核词条
RediSearch 有内置的分词规则(如拼写检查),但它可能不认识你公司的产品名或专业术语。DICT 允许你把这些词保护起来,避免被错误地拆分或纠错。
1 | # 添加词条 |
同义词
除了字典之外,FT.SYN (同义词) 也可以提升搜索体验,主要作用是建立词与词之间的等价关系。这是提升“搜索体验”最直观的方法——用户搜 A,你把包含 B 的结果也给他。常用在缩写、外号、近义词等场景。
1 | # 为索引 idx:emp 添加一组同义词,ID 为 group1 |
DICT 与 SYN 的本质区别
- DICT (词典):纠错/分词保护,“这个词是正确的,别把它改了或拆了。”
- SYN (同义词):查询扩展 (Expansion),“用户搜这个词,其实他也在搜另外那几个词。”