尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从ElasticSearch迁移到RediSearch:性能对比与实战指南

从ElasticSearch迁移到RediSearch:性能对比与实战指南 1. 为什么我要把搜索从 ElasticSearch 换到 RediSearch先交代背景。我手头有一个日增百万级文档的检索服务最早用的是 ElasticSearch 7.x单节点跑在 8C16G 的云主机上索引大概 4000 万条记录每条记录有十来个字段其中三个字段需要全文检索两个字段需要做标签过滤还有一个数值字段要参与排序。业务侧的要求很朴素关键词搜索响应要快过滤要准排序要稳最好还能支持中文分词和前缀补全。一开始 ES 跑得还行但数据量涨到三千万以后问题就来了。查询延迟从几十毫秒慢慢爬到两三百毫秒复杂一点的组合查询直接飙到一秒以上。我试过加节点、调分片、改 refresh_interval、上 SSD效果有但成本也跟着涨。后来一个做实时推荐的朋友跟我说你这种“过滤为主、全文为辅、排序要求高”的场景其实可以试试 RediSearch。我当时的第一反应是Redis 不是做缓存的吗它做搜索能靠谱吗实测下来结论很直接在我这个特定场景里RediSearch 的查询吞吐确实比 ES 高出一大截官方 benchmark 里宣称的“快 5 倍”在过滤加排序这类查询上并不夸张甚至某些纯过滤场景差距更大。但这句话有个前提——它快是因为它做的事情和 ES 不一样。ES 是分布式检索引擎天生为海量数据、复杂相关性打分、水平扩展设计RediSearch 是内存优先的实时二级索引它把索引和文档都放在内存里用 C 写的倒排索引加向量化执行省掉了磁盘 IO 和 JVM 那一层开销。所以这篇不是要劝你把 ES 全删了。我想做的是把这次迁移的完整思路、踩过的坑、参数怎么调、什么场景该用谁原原本本讲清楚。如果你也在被搜索延迟折磨或者正在选型阶段纠结这篇应该能帮你少走几天弯路。适合有 Redis 基础、做过搜索相关开发、想找一个轻量高吞吐方案的同学纯小白也能看懂原理部分但实操建议先补一下 Redis 基础命令。2. 选型背后的逻辑RediSearch 到底快在哪2.1 先搞清楚两者的定位差异很多人一听到“比 ES 快 5 倍”就兴奋但如果不理解快的来源迁移过去大概率会翻车。我用一个生活化的类比来说明。ES 像是一个大型图书馆书放在书架上磁盘你要找书得先查目录卡片倒排索引然后工作人员去书架把书取出来磁盘 IO再根据相关度给你排个序。它的优势是书可以无限多书架可以无限加多个分馆还能协同查询。RediSearch 更像是一个放在你办公桌上的索引卡片盒所有卡片都在手边内存你伸手就能拿到而且卡片本身已经按你要的维度排好了。它的优势是快但桌子就这么大卡片太多就放不下了。这个差异决定了三件事第一RediSearch 的数据量受内存限制第二它的查询延迟极其稳定因为没有磁盘 IO 抖动第三它的分布式能力比 ES 弱很多虽然 Redis 有 Cluster但 RediSearch 在集群下的索引分片和聚合查询要复杂得多。2.2 性能差距的三个技术来源我把实测中感受到的性能差距拆成三个层面这样你在自己场景里也能判断能拿到多少收益。第一是存储介质。ES 的索引存在磁盘上即使有 filesystem cache冷查询还是要读盘。RediSearch 的索引和文档都在内存里查询路径上没有磁盘 IO。这一层在数据量超过内存、cache 命中率下降时差距最明显。我实测中当索引从 1000 万涨到 4000 万ES 的 P99 从 80ms 涨到 400ms而 RediSearch 基本稳定在 30ms 上下因为我的机器内存刚好能装下全部索引。第二是执行引擎。ES 基于 Lucene查询要走 JVM有 GC 停顿有对象序列化开销。RediSearch 是纯 C 实现查询在 Redis 单线程模型里执行没有 GC没有跨语言调用。这一层在简单查询上差距不大但在高并发下 ES 的 GC 抖动会明显拉高尾延迟。第三是查询模型。ES 默认会对每个命中文档计算相关性得分即使你只想要过滤结果。RediSearch 在纯过滤查询FT.SEARCH 配合 FILTER下不做打分直接走索引交集速度极快。这一点是我迁移后收益最大的地方因为我的业务里 70% 的查询其实是“标签过滤 数值排序”根本不需要全文相关性。2.3 什么场景该选 RediSearch什么场景别碰我把判断标准整理成下面这张表你可以直接对照自己的业务。判断维度优先 RediSearch优先 ElasticSearch数据总量能全部放进内存建议留 30% 余量远超内存需要磁盘查询类型过滤、排序、前缀、聚合为主复杂全文相关性、模糊匹配为主延迟要求P99 要求 50ms 以内百毫秒级可接受数据更新高频写入、实时可见批量导入、准实时可接受运维复杂度已有 Redis 集群想复用需要独立搜索集群、专业运维分布式需求单机或简单分片够用需要复杂聚合、跨索引联合查询注意如果你的数据量已经超过单机内存又不想做分片那 RediSearch 不是好选择。硬上的结果就是频繁 eviction查询变慢甚至丢数据。3. 核心细节解析RediSearch 的索引设计与关键参数3.1 索引 schema 怎么定义才不踩坑RediSearch 建索引用FT.CREATE核心是把字段类型和属性定义清楚。我拿自己的业务表举例原始数据大概长这样一条商品记录有 id、title、description、category、brand、price、stock、created_at 这些字段。其中 title 和 description 要全文检索category 和 brand 做标签过滤price 和 created_at 做范围过滤和排序。建索引的命令我写成下面这样FT.CREATE idx:product ON HASH PREFIX 1 product: SCHEMA \ title TEXT WEIGHT 5.0 \ description TEXT WEIGHT 1.0 \ category TAG \ brand TAG \ price NUMERIC SORTABLE \ stock NUMERIC \ created_at NUMERIC SORTABLE这里有几个关键决策我解释一下。TEXT和TAG的区别是新手最容易搞混的。TEXT会做分词适合标题、描述这种自然语言字段TAG不分词适合分类、品牌这种枚举值。我一开始把 category 定义成 TEXT结果搜“手机”的时候把“手机壳”“手机膜”全带出来了因为分词后匹配到了子串。改成 TAG 之后过滤就精确了。WEIGHT是字段权重影响相关性打分。我把 title 设成 5.0description 设成 1.0因为标题命中的相关性明显更高。这个值没有标准答案要根据你的业务反复调。SORTABLE这个属性很关键但也很贵。加了 SORTABLE 的字段RediSearch 会额外维护一份排序索引内存占用会增加。我一开始给所有 NUMERIC 字段都加了 SORTABLE结果内存涨了 40%。后来只给真正需要排序的 price 和 created_at 加内存就降下来了。所以原则是只给排序字段加 SORTABLE过滤字段不加。3.2 中文分词怎么处理RediSearch 默认的分词器对中文支持不好它按空格和标点切中文会整段当成一个 token。我试过直接搜中文基本搜不到。解决方案有两个一是用 RedisJSON 加中文分词插件二是自己在写入前把中文预处理成空格分隔的词。我选的是第二种因为改动最小。写入前用结巴分词把 title 和 description 切成空格分隔的词串再存进 Redis。查询的时候同样对查询词做分词然后用|连接成 OR 查询。这样虽然损失了一些语义但在我的场景里够用了。import jieba def preprocess(text): return .join(jieba.cut(text)) # 写入 title_tokens preprocess(小米手机官方旗舰店) redis.hset(product:1001, mapping{title: title_tokens, ...}) # 查询 query_tokens |.join(jieba.cut(小米手机)) # FT.SEARCH idx:product title:(小米|手机)提示分词后的词串会占用更多内存因为空格也算字符。如果内存紧张可以考虑只对 title 分词description 用原文存但不建 TEXT 索引。3.3 内存估算与容量规划RediSearch 的内存占用大概是原始数据的 1.5 到 3 倍取决于索引字段数量和 SORTABLE 属性。我实测 4000 万条记录原始数据约 12GB建完索引后 Redis 占用约 28GB。所以规划时按原始数据的 2.5 倍估算比较稳妥。具体估算公式可以这样算每个 TEXT 字段的倒排索引大约是字段文本量的 1.2 倍每个 TAG 字段约 0.8 倍每个 SORTABLE NUMERIC 字段约 0.3 倍。再加上文档本身的存储总和就是内存需求。我建议留 30% 余量给 Redis 自身开销和碎片。如果内存不够有几个降级方案减少 TEXT 字段、去掉不必要的 SORTABLE、用MAXMEMORY配合noeviction策略防止数据被淘汰。千万别用allkeys-lru那会把索引数据当缓存淘汰掉查询直接报错。4. 实操过程从 ES 迁移到 RediSearch 的完整步骤4.1 环境准备与版本选择我用的 Redis 7.2 加 RediSearch 2.8这个组合比较稳。安装方式我推荐用 Docker省得编译一堆依赖。docker run -d --name redis-search \ -p 6379:6379 \ -v /data/redis:/data \ redis/redis-stack-server:7.2.0-v9这个镜像自带 RediSearch 和 RedisJSON开箱即用。如果你用云主机注意选内存型实例CPU 核数不用太多因为 Redis 是单线程的4 到 8 核足够。系统我用的 Ubuntu 22.04内核参数调了vm.overcommit_memory1和net.core.somaxconn1024这两个是 Redis 官方建议的。注意不要用redis:latest官方镜像那个不带 RediSearch 模块。必须用redis/redis-stack-server或者自己编译加载模块。4.2 数据迁移的两种方案迁移我试过两种方案各有适用场景。方案一是双写。在应用层同时写 ES 和 Redis跑一段时间对比结果确认无误后切读流量到 Redis最后停掉 ES 写入。这个方案最稳但需要改代码而且双写期间资源翻倍。我最终用的就是这个因为业务不能停。方案二是批量导入。从 ES 导出数据成 JSON再用 Redis 的 pipeline 批量写入。这个方案快但导入期间数据不一致适合能停服的场景。导入脚本核心逻辑如下import json import redis from elasticsearch import Elasticsearch es Elasticsearch(http://localhost:9200) r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def scroll_all(index): resp es.search(indexindex, scroll5m, size1000, body{query: {match_all: {}}}) while True: for hit in resp[hits][hits]: yield hit[_source] resp es.scroll(scroll_idresp[_scroll_id], scroll5m) if not resp[hits][hits]: break pipe r.pipeline(transactionFalse) count 0 for doc in scroll_all(product): key fproduct:{doc[id]} pipe.hset(key, mapping{ title: preprocess(doc[title]), description: preprocess(doc[description]), category: doc[category], brand: doc[brand], price: doc[price], stock: doc[stock], created_at: doc[created_at], }) count 1 if count % 1000 0: pipe.execute() pipe r.pipeline(transactionFalse) print(fimported {count}) pipe.execute()这里用pipeline(transactionFalse)而不是事务因为事务会阻塞其他命令批量导入时性能差很多。每 1000 条执行一次平衡内存和吞吐。4.3 查询改写从 ES DSL 到 RediSearch 命令这是迁移中最费时间的部分。ES 的查询 DSL 很灵活RediSearch 的查询语法相对简单需要重新组织。我举几个典型例子。纯过滤查询。ES 里是bool.filterRediSearch 里用FT.SEARCH配合FILTER参数FT.SEARCH idx:product category:{手机} brand:{小米} \ FILTER price 1000 3000 \ SORTBY price ASC \ LIMIT 0 20全文加过滤。ES 里是must加filterRediSearch 里把全文条件写在 query 里过滤条件用 FILTERFT.SEARCH idx:product (title:(小米|手机)) category:{手机} \ FILTER stock 1 inf \ SORTBY created_at DESC \ LIMIT 0 20聚合查询。ES 的terms聚合RediSearch 用FT.AGGREGATEFT.AGGREGATE idx:product * \ GROUPBY 1 category \ REDUCE COUNT 0 AS cnt \ SORTBY 2 cnt DESC \ LIMIT 0 10改写的时候有个坑RediSearch 的FILTER只支持 NUMERIC 字段TAG 字段的过滤要写在 query 里用field:{value}语法。我一开始把 category 过滤写在 FILTER 里一直报错后来才搞明白。4.4 性能对比实测数据迁移完成后我做了压测用 wrk 模拟 200 并发查询是“分类过滤 价格区间 按销量排序”数据集 4000 万条。结果如下指标ElasticSearch 7.17RediSearch 2.8平均延迟210ms28msP99 延迟680ms45msQPS9505200内存占用32GB含 cache28GB索引构建时间45 分钟12 分钟QPS 差距约 5.5 倍P99 差距约 15 倍。这个结果和官方 benchmark 基本吻合。但我要强调这是在“数据全内存 纯过滤排序”场景下的结果如果你的查询是复杂全文相关性差距会缩小到 2 倍左右。5. 常见问题与排查技巧实录5.1 查询报错与索引问题速查迁移过程中我遇到不少报错整理成下面这张表方便你快速定位。报错信息原因解决方法Unknown index name索引没建或名字写错用FT._LIST确认索引存在Syntax error at offset查询语法错误常见于括号不匹配检查 query 里的括号和引号Cannot filter on non-numeric fieldFILTER 用在了 TAG 字段上TAG 过滤写在 query 里OOM command not allowed内存超限调大 maxmemory 或减少索引字段Index already exists重复建索引先FT.DROPINDEX再建5.2 内存暴涨的排查思路我遇到过一次内存突然从 28GB 涨到 45GB差点把机器打爆。排查过程是这样的先用INFO memory看 used_memory 和 used_memory_rss确认是 Redis 自身占用涨了。然后用FT.INFO idx:product看索引的num_docs和inverted_sz_mb发现文档数没变但索引大小涨了。最后定位到原因有一批新写入的数据里description 字段被塞进了超长文本平均 5000 字导致倒排索引膨胀。解决办法是在应用层限制字段长度超过 2000 字的截断。这个坑很隐蔽因为数据量没变但单条文档的索引开销变大了。提示定期用FT.INFO监控inverted_sz_mb和total_indexing_time发现异常增长及时处理。5.3 排序结果不一致的问题迁移后业务反馈说排序结果和 ES 不一样。排查发现是浮点数精度问题。ES 里 price 存的是 floatRediSearch 的 NUMERIC 存的是 double理论上更精确但我的数据里有 9.9 和 9.90 这种ES 排序时当成相等RediSearch 也当成相等但和另一个字段组合排序时顺序就变了。解决办法是统一排序字段的精度在写入前用round(price, 2)处理。另外 RediSearch 的 SORTBY 支持多字段语法是SORTBY 4 price ASC created_at DESC数字 4 表示后面跟 4 个参数。这个数字容易写错写错了会报语法错误。5.4 高并发下的连接池配置RediSearch 单线程处理命令高并发下连接池配置很关键。我一开始用默认配置200 并发时大量超时。后来调整了客户端连接池参数最大连接数设成 50最小空闲 10超时 2 秒。服务端把maxclients调到 10000timeout设成 0 禁用空闲断开。还有一个技巧是用FT.SEARCH的NOCONTENT参数。如果你只需要文档 ID 不需要内容加上这个参数可以省掉序列化开销QPS 能再提升 30% 左右。我的列表页只需要 ID 再批量取详情所以用上了这个。FT.SEARCH idx:product category:{手机} NOCONTENT LIMIT 0 206. 我踩过的坑和几条实在建议第一个坑是别把 RediSearch 当 ES 用。我一开始想用它做复杂相关性排序结果发现它的打分模型比 ES 简单太多TF-IDF 加字段权重没有 BM25 那些高级特性。如果你的业务强依赖相关性质量ES 还是更合适。第二个坑是持久化配置。Redis 默认 RDB 加 AOF我为了性能一度关了 AOF结果一次意外重启丢了半小时数据索引重建花了 12 分钟。后来改成appendfsync everysec性能损失很小但数据安全多了。搜索索引虽然可以从源数据重建但重建期间服务不可用这个代价要算进去。第三个坑是集群下的索引分片。Redis Cluster 里每个 key 按 slot 分布RediSearch 的索引是建在单个节点上的跨节点的聚合查询需要客户端自己做 merge。我试过用 Redis Cluster 跑 RediSearch复杂度比单机高很多最后退回了单机加主从。如果你的数据量单机放得下强烈建议别上 Cluster。最后分享一个实用技巧用FT.EXPLAIN命令查看查询的执行计划。它会告诉你查询被解析成什么样子哪些条件走了索引哪些是后过滤。我优化查询时经常用它能快速发现写错的查询语法。FT.EXPLAIN idx:product category:{手机} brand:{小米}这个命令的输出会显示倒排索引的合并方式如果看到FULL SCAN就说明没走索引需要检查字段定义。我靠它发现过一个 TAG 字段因为大小写不一致导致索引失效的问题改成统一小写后查询快了 10 倍。数据量在内存放得下、查询以过滤排序为主、已经有 Redis 运维经验的团队RediSearch 值得认真考虑。但如果你需要复杂全文相关性、数据量远超内存、或者团队对 Redis 不熟那还是老老实实用 ES别为了追性能给自己挖坑。选型这件事适合的才是最快的。
返回列表