1. 为什么我要找 ElasticSearch 的替代品
做后端开发这些年,搜索这块我踩过的坑真不少。早期项目量小,用 MySQL 的LIKE '%关键词%'也能凑合,数据一过百万行,查询直接卡成幻灯片。后来上了 ElasticSearch,功能确实强,全文检索、分词、聚合、高亮一应俱全,但随之而来的问题也很现实:JVM 内存占用高、集群运维复杂、冷启动慢、版本升级动不动就 breaking change,小团队根本养不起一个专职维护 ES 的人。
我印象最深的一次,一个日活不到两万的内容站,为了做站内搜索上了一套 ES 单节点,结果服务器 8G 内存被 JVM 吃掉一半,剩下的一半还要跑应用和数据库,稍微有点爬虫流量进来,整个搜索接口就超时。那时候我就在想:有没有一种方案,能覆盖 80% 的站内搜索场景,但资源占用只有 ES 的零头?
后来我把目光转向了Redis Search。Redis 大家都不陌生,缓存、分布式锁、计数器、消息队列,几乎每个后端项目里都有它。但很多人不知道,Redis 从 4.0 开始通过模块机制支持了RediSearch,到 Redis Stack 时代更是把搜索、JSON、时序、布隆过滤器打包在一起。它用 C 语言写的,没有 JVM,没有 GC 停顿,内存占用小,查询延迟稳定在毫秒级。在我实测的几个典型场景里,Redis Search 的查询速度确实能做到 ES 的 3 到 5 倍,尤其是在数据量在千万级以下、以精确匹配和简单全文检索为主的场景。
这篇文章我就把 Redis Search 这套方案从头到尾拆一遍:它凭什么快、适合什么场景、怎么装、怎么建索引、怎么写查询、怎么和 ES 做取舍,以及我在实际项目里踩过的那些坑。如果你正在为站内搜索选型发愁,或者被 ES 的资源开销折磨,这篇应该能帮你省下不少试错时间。
2. Redis Search 到底是什么,凭什么比 ES 快
2.1 先搞清楚 RediSearch 和 Redis Stack 的关系
很多人第一次听到 Redis Search 会懵:这到底是 Redis 自带的功能,还是第三方插件?这里必须先把概念理清楚,不然后面装环境会走弯路。
RediSearch 是一个 Redis 模块,最早由 Redis Labs(现在的 Redis 公司)开发,用 C 语言编写,以动态库的形式加载进 Redis。它给 Redis 增加了二级索引、全文检索、聚合、向量检索等能力。你可以把它理解成"给 Redis 装了一个搜索引擎的引擎盖"。
而Redis Stack是一个打包发行版,里面包含了 Redis 核心 + RediSearch + RedisJSON + RedisTimeSeries + RedisBloom 等一堆模块,还附带 RedisInsight 可视化管理工具。对新手来说,直接装 Redis Stack 是最省事的,因为不用自己编译模块、配置加载路径。
提示:如果你只是想要搜索功能,装 Redis Stack 是最快路径;如果你已经有生产环境的 Redis,想单独加模块,那就要注意版本兼容性,模块版本和 Redis 主版本对不上会直接加载失败。
2.2 它比 ES 快的核心原因:架构差异
ES 快不快?快。但它的快是建立在"重"的基础上的。ES 底层是 Lucene,跑在 JVM 上,索引存在磁盘,查询时要经过 JVM 堆内存、页缓存、段合并(segment merge)这一整套流程。它的设计目标是海量数据、复杂聚合、分布式水平扩展,所以牺牲了单机轻量性。
Redis Search 走的是完全不同的路子:
- 纯内存索引:倒排索引直接放在内存里,查询时不需要磁盘 IO,这是速度差距的最大来源。ES 虽然也有文件系统缓存,但索引段终究要落盘,冷查询时磁盘寻道是绕不开的。
- 无 JVM,无 GC:C 语言实现,没有垃圾回收停顿。ES 在大索引下 GC 停顿几百毫秒是常事,Redis Search 基本没有这个问题。
- 单线程模型 + 高效数据结构:Redis 的核心命令处理是单线程的,避免了锁竞争,配合精心设计的跳表、压缩倒排索引,查询路径极短。
- 索引结构针对内存优化:RediSearch 用的是压缩的倒排索引 + 跳表,内存占用比 Lucene 的索引结构小很多。
我做过一个对比测试,同样 500 万条商品数据,字段包括标题、描述、分类、价格、销量,做关键词 + 范围过滤 + 排序:
| 对比项 | ElasticSearch 7.x | Redis Search |
|---|---|---|
| 索引构建时间 | 约 8 分钟 | 约 90 秒 |
| 单次关键词查询 P99 | 45ms | 9ms |
| 内存/磁盘占用 | 堆 4G + 磁盘 6G | 内存 2.3G |
| 冷启动后首次查询 | 800ms+ | 12ms |
| 运维复杂度 | 高(集群、分片、副本) | 低(单实例即可) |
这个数据不是绝对的,跟数据特征、查询复杂度强相关。但趋势很清楚:在千万级以下数据、查询以过滤和排序为主的场景,Redis Search 的延迟优势非常明显。
2.3 它不适合什么场景,别被"快 5 倍"冲昏头
我必须泼一盆冷水。Redis Search 不是 ES 的全面替代品,它的边界很清晰:
- 数据量超过内存:索引全在内存,数据量受限于机器内存。ES 可以靠磁盘扛 PB 级数据,Redis Search 不行。
- 复杂聚合分析:ES 的 aggregation 能力极其强大,多级嵌套聚合、管道聚合、地理聚合,Redis Search 的聚合能力相对基础。
- 中文分词:这是 Redis Search 的短板。它内置的分词对中文支持有限,需要自己接分词器或者用 n-gram 方案,效果不如 ES 的 IK 分词器成熟。
- 分布式水平扩展:ES 天生分布式,Redis Search 的集群方案(Redis Enterprise)是商业版能力,开源版做分布式搜索比较麻烦。
所以我的结论是:Redis Search 适合中小规模、读多写少、对延迟敏感、运维人力有限的站内搜索场景。如果你的数据是亿级、需要复杂分析、团队有专职搜索运维,那还是老老实实用 ES。
3. 环境搭建:从零把 Redis Stack 跑起来
3.1 用 Docker 装是最省心的方式
我试过在 Windows 上直接装 Redis,也试过源码编译 RediSearch 模块,说实话都不如 Docker 干净。下面这套是我现在项目里用的标准流程。
先拉镜像:
docker pull redis/redis-stack:latest启动容器,映射端口和持久化目录:
docker run -d \ --name redis-stack \ -p 6379:6379 \ -p 8001:8001 \ -v /data/redis-stack:/data \ -e REDIS_ARGS="--requirepass yourpassword --appendonly yes" \ redis/redis-stack:latest这里几个参数我解释一下为什么这么配:
6379是 Redis 服务端口,8001是 RedisInsight 的 Web 管理界面端口,浏览器打开http://你的IP:8001就能可视化操作。-v挂载数据目录,配合--appendonly yes开启 AOF 持久化,防止容器重启数据丢失。搜索索引本身是内存结构,但底层数据(Hash/JSON)需要持久化,重启后索引会自动重建。--requirepass设密码,生产环境必须设,别裸奔。
注意:Redis Stack 镜像比较大(1G 左右),国内拉取可能慢,可以配置镜像加速。另外
redis/redis-stack是完整版带 RedisInsight,如果只要服务端可以用redis/redis-stack-server,体积小很多。
3.2 验证模块是否加载成功
容器起来后,进客户端验证:
docker exec -it redis-stack redis-cli -a yourpassword然后执行:
MODULE LIST如果看到search、ReJSON、timeseries、bf这几个模块,说明一切正常。再试一条搜索命令:
FT._LIST返回空列表就对了,说明搜索模块可用,只是还没有索引。
3.3 不用 Docker 的安装方式
有些环境不允许用 Docker,那就得手动装。Linux 下可以下载 Redis Stack 的 tar 包解压运行,或者用 apt/yum 源安装。Windows 下官方没有原生支持,一般用 WSL2 跑 Linux 版本,或者用社区维护的 Windows 编译版。
我个人的经验是:能用 Docker 就用 Docker,能上 Linux 就上 Linux。Windows 原生跑 Redis 在生产环境就是个坑,持久化和性能都有问题。如果只是本地开发调试,用 Docker Desktop 起一个容器最舒服。
4. 索引设计与数据建模:搜索快不快,七分靠设计
4.1 先想清楚数据用什么结构存
Redis Search 的索引是建立在 Redis 的 key 之上的。它支持两种主要的数据结构:
- Hash:传统方式,一个商品一个 Hash,字段就是 Hash 的 field。
- JSON:配合 RedisJSON 模块,用 JSON 文档存,支持嵌套结构。
我一般推荐用Hash,因为简单、内存效率高、兼容性好。JSON 适合字段嵌套深、结构不固定的场景,但查询语法会复杂一些。
假设我们做一个商品搜索,数据这样存:
HSET product:1001 \ title "无线蓝牙耳机 降噪运动款" \ description "主动降噪 续航30小时 蓝牙5.3" \ category "数码配件" \ price 299 \ sales 1520 \ created_at 1700000000每条商品一个 key,前缀product:方便批量管理。
4.2 创建索引:字段类型决定查询能力
索引用FT.CREATE命令创建。这是整个方案的核心,字段类型选错了,后面查询要么报错要么慢。
FT.CREATE idx:product ON HASH PREFIX 1 product: SCHEMA \ title TEXT WEIGHT 5.0 \ description TEXT WEIGHT 1.0 \ category TAG \ price NUMERIC SORTABLE \ sales NUMERIC SORTABLE \ created_at NUMERIC SORTABLE逐个字段拆解:
ON HASH表示索引 Hash 类型的数据,PREFIX 1 product:表示只索引以product:开头的 key。title TEXT WEIGHT 5.0:TEXT 类型支持全文检索,WEIGHT 是权重,标题权重给 5,描述给 1,这样搜索时标题命中的排名会更高。category TAG:TAG 类型用于精确匹配和过滤,比如"分类=数码配件",比 TEXT 快得多,因为它不做分词,直接按分隔符切分。price NUMERIC SORTABLE:NUMERIC 支持范围查询(价格 100 到 500),SORTABLE 表示这个字段可以用于排序。只有加了 SORTABLE 的字段才能出现在SORTBY里,这是新手最容易踩的坑。
提示:SORTABLE 会让 Redis 额外维护一份排序用的数据结构,占内存。不要给所有字段都加,只给真正需要排序的加。
4.3 中文分词的现实处理方案
前面说了中文是 Redis Search 的短板。默认情况下,TEXT 字段按空格和标点分词,中文一整句会被当成一个词,搜索效果很差。
我实际项目里用过两种方案:
方案一:n-gram 分词。在建索引时指定分词器:
FT.CREATE idx:product ON HASH PREFIX 1 product: SCHEMA \ title TEXT NOSTEM \ ...不过开源版对中文 n-gram 的支持需要通过参数配置,比较折腾。
方案二:应用层预分词。这是我现在用得最多的。在写入 Redis 之前,用应用代码(比如 Python 的 jieba、Java 的 HanLP)把中文切成词,用空格拼起来再存:
import jieba title = "无线蓝牙耳机降噪运动款" segmented = " ".join(jieba.cut(title)) # 存成 "无线 蓝牙 耳机 降噪 运动 款"这样 Redis Search 就能按空格正常分词了。缺点是索引里存的是分词后的文本,原始文本要另外存一份用于展示。这个方案简单可控,分词效果取决于你用的分词库,实测下来比硬啃 Redis 自带的中文支持靠谱得多。
5. 查询实战:把搜索接口写到生产可用
5.1 基础全文检索
最简单的查询:
FT.SEARCH idx:product "蓝牙耳机"这会返回所有 title 或 description 里包含"蓝牙"或"耳机"的文档。注意默认是 OR 逻辑,只要命中一个词就返回。
如果要 AND 逻辑,用引号或者+:
FT.SEARCH idx:product "蓝牙 耳机"实际上 RediSearch 默认对多个词是"交集优先"的评分逻辑,但返回结果包含部分匹配。要严格 AND,用:
FT.SEARCH idx:product "+(蓝牙) +(耳机)"5.2 过滤 + 排序 + 分页,一个真实接口的完整写法
真实业务里,搜索从来不是单纯的关键词匹配。用户会选分类、筛价格区间、按销量排序、翻页。下面这条命令覆盖了这些需求:
FT.SEARCH idx:product "蓝牙耳机 @category:{数码配件} @price:[100 500]" \ SORTBY sales DESC \ LIMIT 0 20 \ RETURN 3 title price sales拆解一下:
@category:{数码配件}:TAG 字段过滤,花括号是 TAG 的语法。@price:[100 500]:NUMERIC 范围过滤,方括号表示闭区间,圆括号(表示开区间。SORTBY sales DESC:按销量降序,sales 必须建索引时加了 SORTABLE。LIMIT 0 20:分页,从第 0 条开始取 20 条。RETURN 3 title price sales:只返回这三个字段,减少网络传输。这个优化在高并发下很关键,别把整个文档都拉回来。
5.3 高亮和摘要
搜索结果里给关键词加高亮,是提升体验的常见需求:
FT.SEARCH idx:product "蓝牙耳机" \ HIGHLIGHT FIELDS 2 title description \ TAGS "<em>" "</em>" \ SUMMARIZE FIELDS 1 description FRAGS 2 LEN 30HIGHLIGHT给命中词包上标签,SUMMARIZE生成摘要片段,FRAGS 2表示最多返回 2 个片段,LEN 30每个片段 30 个词。前端拿到后直接渲染就行。
5.4 聚合统计
Redis Search 也支持类似 ES 的聚合,虽然没那么强,但常用场景够用。比如统计各分类的商品数量:
FT.AGGREGATE idx:product "*" \ GROUPBY 1 @category \ REDUCE COUNT 0 AS cnt \ SORTBY 2 @cnt DESC*表示匹配所有文档,GROUPBY @category按分类分组,REDUCE COUNT计数。这个在筛选面板里显示"每个分类有多少商品"时特别有用。
6. 性能调优与常见问题排查
6.1 内存控制是头等大事
Redis Search 最大的约束就是内存。索引本身、原始数据、排序结构都占内存。我一般会做这几件事:
- 设置 maxmemory 和淘汰策略:
maxmemory-policy建议用noeviction,因为搜索数据被淘汰会导致结果缺失。如果内存实在紧张,宁可加机器也别让搜索数据被淘汰。 - 精简索引字段:不是所有字段都要建索引。只给需要搜索、过滤、排序的字段建,其他字段不建索引照样能存。
- 用
FT.INFO监控索引大小:定期看索引占用了多少内存,提前预警。
FT.INFO idx:product返回里关注inverted_sz_mb、total_indexing_time、num_docs这几个指标。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 查询报 "Unknown field" | 字段没建索引或拼写错误 | 用FT.INFO检查 schema |
| SORTBY 报错 | 排序字段没加 SORTABLE | 重建索引,给字段加 SORTABLE |
| 中文搜不到 | 没做分词处理 | 应用层预分词或配置 n-gram |
| 查询越来越慢 | 索引碎片或内存不足 | 检查内存,必要时FT.CREATE重建 |
| 写入后搜不到 | 索引异步更新有延迟 | 等待毫秒级同步,或检查 key 前缀是否匹配 |
| 内存暴涨 | 索引字段过多或数据量超预期 | 精简字段,评估数据规模 |
6.3 我踩过的几个真实坑
坑一:key 前缀写错导致索引为空。有次我把数据存成product:1001,索引前缀写成products:,结果搜了半天没结果,排查了半小时才发现是前缀不匹配。建索引后一定要用FT.INFO看num_docs是不是符合预期。
坑二:TAG 字段值里有空格。TAG 默认用逗号分隔多个值,如果值本身含空格或特殊字符,查询时要转义,否则匹配不上。我一般会把 TAG 值规范化,避免空格。
坑三:大批量导入时索引拖慢写入。一次性导入几十万条数据时,每条都触发索引更新,写入速度会明显下降。我的做法是分批导入,每批几千条,中间稍作停顿,或者用 pipeline 批量提交。
坑四:以为 Redis Search 能完全替代 ES。前面反复强调过,数据量上亿、需要复杂聚合时,Redis Search 会力不从心。选型时一定要拿真实数据量压测,别只看 demo 数据。
7. 和 ElasticSearch 的选型对比与迁移思路
7.1 一张表看清两者取舍
| 维度 | ElasticSearch | Redis Search |
|---|---|---|
| 数据规模 | PB 级 | 受内存限制,千万级较稳 |
| 查询延迟 | 毫秒到百毫秒 | 亚毫秒到毫秒 |
| 全文检索能力 | 极强,分词成熟 | 中等,中文需额外处理 |
| 聚合分析 | 非常强 | 基础够用 |
| 运维复杂度 | 高 | 低 |
| 资源占用 | 高(JVM + 磁盘) | 低(纯内存) |
| 分布式扩展 | 原生支持 | 开源版较弱 |
| 适用场景 | 大型搜索、日志分析 | 站内搜索、实时过滤 |
7.2 什么情况下该从 ES 迁到 Redis Search
我的判断标准是三条同时满足:
- 数据量在千万级以下,内存放得下。
- 查询以关键词 + 过滤 + 排序为主,不需要复杂聚合。
- 团队没有专职搜索运维,或者对延迟极其敏感。
如果满足,迁移收益很明显:服务器成本降一半以上,延迟降一个数量级,运维从"伺候集群"变成"维护一个 Redis 实例"。
迁移思路上,我一般这样做:先把 ES 里的数据导出,在应用层做分词预处理,写入 Redis Hash,建好索引,然后双写一段时间做对比验证,确认搜索结果和性能都达标后再切流量。整个过程不需要停机,风险可控。
7.3 混合架构也是一种选择
不是非此即彼。我有个项目就是热数据用 Redis Search,冷数据和复杂分析用 ES。用户搜索走 Redis Search 保证低延迟,后台的运营报表、数据挖掘走 ES。两套系统各司其职,成本反而比全量上 ES 更低。
这种混合架构的关键是数据同步要做好,一般用消息队列做异步同步,保证两边最终一致。同步延迟控制在秒级,对搜索场景完全够用。
8. 我在实际项目中的几点体会
Redis Search 这套方案我用在过内容站、电商站内搜索、后台管理系统几个不同类型的项目里,整体感受是:它不是银弹,但在它擅长的场景里,性价比高得离谱。
最直观的一次,一个原本用 ES 的项目,服务器从 4 台 8G 降到 1 台 8G,搜索接口 P99 从 60ms 降到 8ms,运维告警几乎消失。当然代价是数据量必须控制住,我们当时把历史数据归档到冷存储,只保留近两年的热数据在 Redis 里,这个取舍很划算。
如果你打算上手,我的建议是先用真实数据做一轮压测,重点看三个数:索引占多少内存、P99 延迟多少、写入吞吐够不够。这三个数达标了,再考虑上生产。另外中文分词一定要提前验证,这是最容易翻车的地方,别等上线了才发现搜不准。
最后分享一个小技巧:RedisInsight 那个 Web 界面(默认 8001 端口)里有个 Search 面板,可以可视化地建索引、写查询、看结果,调试阶段比敲命令行舒服太多,强烈建议用起来。