news 2026/10/9 17:38:48

Redis商品搜索架构:十万级数据10毫秒查询的轻量级方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis商品搜索架构:十万级数据10毫秒查询的轻量级方案

做电商后台,商品搜索这一块迟早要面对。很多团队一提到“搜索”,下意识就想去部署一套重型全文检索引擎,结果机器资源、索引维护、集群调优全压上来,一个小模块搞得比订单系统还重。我在之前的项目里用 Redis 从零搭了一套商品搜索,十万级商品量,单实例扛住了线上日常流量,查询平均在 10 毫秒以内,索引构建和更新也完全在可控范围内。这套方案的本质,是把 Redis 的有序集合当成倒排索引来用,配合哈希、集合做过滤和详情存储,再用缓存、布隆过滤器、分布式锁把并发和穿透问题兜住。这篇就把整套架构拆开讲,适合商品量在百万级以内、搜索需求以“关键词加筛选排序”为主、又不想为新模块引入重系统的团队参考。

1. 轻量级索引方案的整体设计与取舍

1.1 为什么是 Redis 而不是重型检索组件

先聊选型逻辑。商品搜索的诉求无非几个:按关键词召回、按类目品牌价格过滤、按销量时间排序、分页返回。重型检索引擎确实能做得很好,分词、相关性排序、分布式扩展都专业,但代价是集群节点变多、索引分片要规划、内存占用翻倍,还得有人专人维护。对一个“搜索功能只是辅助转化入口”的业务来说,这套成本经常是不划算的。

Redis 方案的思路很直接:用有序集合(ZSet)存储每个词对应的商品 ID 列表,用分数(score)表达排序权重;再用哈希(Hash)存商品详情字段;用集合(Set)或有序集合存类目、品牌这些过滤维度。查询时做集合运算取交集,最后按分数倒序取一页数据。整个过程都在内存里完成,单次查询没有跨网络调用组件,延迟自然低。

这个方案适用边界也很清晰:商品量百万级以内、搜索逻辑是“关键词 + 结构化筛选”、允许商品上架后几秒内被搜到、团队没有专职搜索引擎运维人员。如果业务有同义词挖掘、纠错、语义召回这类强检索需求,那还是老老实实上专业组件,Redis 在这块做不到同等效果。做技术选型,先认清楚边界,比选一个“看起来厉害”的方案重要得多。

1.2 三类核心数据结构的搭配方案

Redis 里有五种基础数据类型,真正承担搜索任务的实际上是 ZSet、Hash、Set 这三类,String 在缓存和布隆过滤器里打辅助。

ZSet 是这套索引的核心。一个搜索词对应一个 key,比如idx:word:连衣裙,member 是商品 ID,score 是排序权重。查询“连衣裙”就是ZREVRANGE idx:word:连衣裙 0 19,直接按权重倒序取前 20 个商品 ID。这个词条覆盖的商品越多,这个 key 就越大,所以后面会讲到对大词做截断。

Hash 负责存商品详情,key 形如goods:info:{id},字段包括标题、类目、品牌、价格、状态、销量、上架时间等。查询结果拿到商品 ID 后,用HMGET批量取详情,组装返回。这里有一个实际经验:尽量不要把整个商品详情序列化成 JSON 塞进一个 String,字段更新时要整体覆盖,内存利用率差,且无法对单字段做更新。拆成 Hash 的多个 field,既省内存,又能单独更新销量、价格这种高频变化字段。

Set 用于维护过滤维度的成员关系,比如valid:ids保存所有上架商品 ID,brand:{品牌ID}保存某个品牌下的所有商品 ID。过滤时用SINTER对候选结果做交集。如果过滤维度本身要参与排序,就把它也建成 ZSet,score 统一置 0,查询交集时用AGGREGATE MAX,这样最终 score 还能保留原排序值。这个细节很多人会忽略,后面在故障复盘里还会提。

1.3 整体数据流与适用范围

整套链路是这样的。商品上架或变更时,服务从数据库拿到商品信息,走分词、权重计算,然后通过 Redis pipeline 一次性写入词条倒排索引、商品详情 Hash、类目和品牌索引。搜索请求进来后,先对 query 分词,分词结果作为关键词去查倒排索引,多个词之间取交集;接着依次做类目、品牌、上架状态过滤;再根据排序模式取对应 ZSet 的一段数据;最后批量查 Hash 组装结果返回。

缓存层放在查询和索引之间。搜索结果按“关键词 + 过滤条件 + 排序 + 页码”维度做短时间缓存,热点词首屏也可以做进程内缓存。同时用布隆过滤器拦截“根本没索引过的词”,避免无效查询把流量打到底层。

这套结构如果用一个词概括,就是“索引外置、查询直取、缓存兜底”。Redis 在这里面充当的既是一个存储引擎,也是一个计算引擎,很多集合运算直接在服务端完成,减少了应用层的数据搬运。对于十万级商品,整个索引内存占用大概在 500MB 到 1GB 之间,单机完全能扛。

2. 商品索引的构建、权重与内存治理

2.1 分词策略:词库切分与二元切分混用

商品搜索的召回效果,一半以上取决于分词质量。Redis 只是个存储和计算的壳,词切得烂,后面索引和查询都白搭。

我在项目中采用的是“词库切分 + 二元切分”混合策略。先基于一个通用中文词库,把商品标题按词库匹配切出标准词,比如“男士纯棉白色短袖T恤”切成“男士 / 纯棉 / 白色 / 短袖 / T恤”。同时保留二元切分结果作为补充,把相邻两个字符组成一个词,比如“纯棉”如果词库里没有,也能通过 bigram 切成“纯棉”作为索引词。这种做法牺牲了一部分索引精度,但能显著提升词库外的召回率,代码也简单,不需要维护巨大的自定义词典就能覆盖绝大多数商品标题。

类目词和品牌词要单独处理。类目 ID 直接作为过滤条件,不参与分词;品牌名要单独切成品牌词,做品牌搜索时命中率更高。比如“Apple iPhone 15 Pro 手机壳”,品牌词“Apple”和通用词“手机壳”分开索引。还有一个容易被忽略的点:英文词要统一转小写,数字和单位要规范化,避免“T恤”和“t恤”、“5G”和“5 g”这种同义不同词的索引分裂。做完规范化再去构建索引,能少踩很多召回不全的坑。

2.2 索引写入与删除的关键步骤

索引写入最忌讳逐条命令执行,几十个词条就是几十次网络往返,性能完全没法看。正确做法是使用 pipeline 批量提交。核心流程代码如下:

import redis r = redis.Redis(host="10.0.0.5", port=6379, db=0, decode_responses=True) def calc_score(goods): # sales_norm 最近7天销量归一化到 0~100 # recency_bonus 时间衰减因子,越新权重越高 return sales_norm * 0.7 + recency_bonus * 0.3 def index_goods(goods): words = segment(goods["title"]) + [goods["brand"]] words = dedup_words(words) pipe = r.pipeline(transaction=False) for w in words: key = f"idx:word:{w}" pipe.zadd(key, {goods["id"]: calc_score(goods)}) # 这里限制单个词条最大长度,防止大词膨胀 if r.zcard(key) < 5000: pass pipe.hset(f"goods:info:{goods['id']}", mapping={ "title": goods["title"], "price": goods["price"], "brand": goods["brand"], "category_id": goods["category_id"], "status": goods["status"], "sales": goods["sales"], "on_time": goods["on_time"], }) pipe.zadd(f"filter:category:{goods['category_id']}", {goods["id"]: 0}) pipe.zadd(f"filter:brand:{goods['brand']}", {goods["id"]: 0}) pipe.sadd("valid:ids", goods["id"]) pipe.execute()

删除商品时,必须先拿到该商品当前标题分词得到的全部旧词,再逐个从倒排索引里移除,否则会残留旧索引,搜索还能搜到下架商品。这也是很多团队上线后遇到“商品明明下架了还能搜到”的直接原因。删除流程同样走 pipeline,最后删除商品详情 Hash 和过滤索引中的成员。

这里多提一句,上线索引变更不要直接操作线上的 key。我的习惯是先构建一套带版本号的索引 key,比如idx:word:v2:连衣裙,全部构建完成后再通过配置中心切换查询路由版本号,实现平滑过渡。商品量不大时,直接低峰期重建后切换也问题不大,但一定要留回滚方案。

2.3 排序权重设计

ZSet 的 score 决定了商品在搜索结果里的位置,这个值的设计要结合业务,不是简单拿销量一放了事。我用的公式是:

score = 近7天销量归一化值 × 0.7 + 上架时间衰减分 × 0.3

销量归一化可以采用sqrt(销量)或者对数缩放,避免头部爆款把长尾商品全部压到后面。时间衰减分可以设计成类似30 × exp(-上架天数 / 7)这样,新品有初始加权,越老权重衰减越快。这样综合下来,一个刚上架但有基础销量的商品能排到前面,老商品也不会彻底被埋没。

需要注意一个技术细节:Redis 的 score 是 IEEE 754 双精度浮点数,超出 2^53 会丢失精度,所以不要把毫秒时间戳直接塞进 score 做时间排序。正确做法是先归一化成相对分数,或者单独维护一个“按上架时间排序”的 ZSet,score 使用秒级时间戳。排序维度多时,我建议直接为每个排序维度建独立的 ZSet,比如sort:price:asc、sort:new:desc,避免在 score 里做复杂的位运算。权重公式一旦定了,尽量不要频繁调整,否则要遍历所有词条重算 score,成本很高。

2.4 内存估算与容量规划

内存规划决定方案能扛多大商品量。按我实际项目里的统计,单商品索引的内存开销大致这样分布:

  • 倒排索引:一个商品平均参与 15 个词条索引,每个词条成员包含商品 ID、score 和跳表指针,大约 100 到 150 字节,合计 1500 到 2200 字节。
  • 商品详情 Hash:20 个字段以内,大约 300 字节。
  • 过滤索引:类目、品牌、有效集合各一个成员关系,合计约 200 字节。

一台承载 10 万商品搜索索引的 Redis 实例,内存消耗在 500MB 到 1GB 区间;100 万商品大概需要 5GB 到 10GB。单实例控制在 10GB 以内比较稳妥,超过这个量级就要考虑分片或者换方案了。

内存治理上还有两个硬性措施。第一,对高频大词做截断:像“装”“女”“新款”这种热门词,倒排列表可能覆盖几十万商品,单个 key 过大会成为 bigkey,而且查询时取交集成本极高。我的做法是每个词条最多保留排序前 5000 个商品 ID,既控制内存,也保证查询时效,代价是深度长尾结果可能召回不全,业务完全可接受。第二,设置合理的 maxmemory-policy,搜索专用实例建议直接noeviction,避免 Redis 自动淘汰索引数据导致查询结果不完整,这一点很多人会踩坑。

3. 搜索查询链路的工程实现

3.1 单关键词与多关键词检索

单关键词查询最简单,分词后拿到唯一词,直接查idx:word:{词}的 ZSet,ZREVRANGE按 score 倒序取一段即可。比如用户搜“连衣裙”,分词结果就是“连衣裙”,一条命令搞定。

多关键词就要做交集。用户搜“男士 黑色 连衣裙”,分词得到三个词,倒排索引分别对应三个 ZSet。交集有两种做法:

一种是客户端聚合。用ZRANGEBYSCORE把每个词的前 N 个商品 ID 取回应用层,再用哈希集合求交集。这个方案适合单词条商品数较少的情况,比如 5000 以内,控制取回数量后速度很快,且不占用 Redis 额外内存。

另一种是服务端聚合,使用ZINTERSTORE命令直接对多个 ZSet 求交集写入临时 key,再读结果。这个方案的优点是原子,缺点是会生成临时 key,并发高时会带来内存抖动,且因为 Redis 单线程执行,大集合求交会阻塞其他命令。所以服务端聚合只建议用在词少、单词商品量可控的轻量场景。

实际工程里我一般这么处理:单词商品数小于 2000 用客户端聚合,大于 2000 或词数较多时用服务端聚合加超时保护。生死原则是控制单个词条规模,也就是前面说的截断策略,这会让交集成本始终处于可控范围。

3.2 类目、品牌、价格过滤的实现顺序

过滤条件决定了最终返回给用户的商品范围。常见的过滤维度包括类目、品牌、价格区间、库存状态。这里的核心问题是:过滤在哪个环节做、用什么数据结构做。

类目和品牌过滤我用 ZSet 做,key 分别是filter:category:{id}和filter:brand:{id},member 是商品 ID,score 统一为 0。和搜索结果 ZSet 求交集时用ZINTERSTORE并指定AGGREGATE MAX,这样最终得分保留的是搜索词条里的排序 score,不会因为两个 ZSet 的 score 相加而乱序。

价格区间过滤有两种处理方式。如果候选结果集不大,比如已过滤到几百个商品 ID,直接在应用层批量查 Hash,按价格字段过滤,简单可靠。候选结果集很大时,维护一个sort:price:ascZSet,score 就是商品价格,先用ZRANGEBYSCORE取出价格区间内的商品 ID,再和候选结果求交集。这里的关键是过滤顺序:优先做能把候选集大幅缩小的过滤,比如热门类目先过滤类目,冷门词先过滤词条,把最后需要处理的商品数量压到最小。

上架状态过滤我单独用一个valid:idsSet 维护所有可售商品 ID。最后一步用SINTER和候选结果求交,避免把下架商品返回给用户。这个集合更新要跟商品上架下架消息保持同步,这里同步失败会导致搜索结果异常,所以要放到和索引更新同一个异步任务里处理。

3.3 分页、排序与前缀联想

分页直接基于排序后的 ZSet 做:ZREVRANGE key offset offset+page_size-1取出当前页的商品 ID。但深分页是这套方案最明显的短板,offset 深了以后,Redis 需要跳过大量元素,耗时随 offset 线性上涨。我的处理是限制 offset 最大 10000,超过直接提示用户“请缩小筛选范围”,既保护 Redis 也保护用户体验。

排序这块,默认综合排序直接用搜索词条的 score,价格升序降序用一个独立的sort:price:ascZSet,新品排序用sort:new:descZSet,score 是上架时间的秒级时间戳。查询时根据用户选择的排序模式选择对应的 ZSet 取数据。这里我会在返回结果前做一个去重操作,因为多词搜索时同一个商品可能命中多个词条但只保留一次。

前缀联想是搜索框体验的一部分。Redis 的 ZSet 有个特性:当所有 member 的 score 相同时,可以用ZRANGEBYLEX按字典序做范围查询。我维护一个prefix_poolZSet,把所有搜索词存进去,score 统一 0。用户输入“男”的时候,执行:

ZRANGEBYLEX prefix_pool [男 [男\xff

就能拿到所有以“男”开头的历史搜索词和商品词,再按词频排序取前 10 个返回。这个方案的代价是词量大了以后每次 lex 范围扫描有一定开销,所以只对前 1 到 3 个字符做前缀提示,太长的前缀直接停止联想,性能足够支撑日常搜索框的请求量。

3.4 查询性能优化手段

查询链路里最容易耗时的环节在“拿到商品 ID 后组装详情”这一段。如果每个商品 ID 单独查一次 Hash,20 个商品就要 20 次 RTT,网络开销直接拖慢整个请求。这里必须用 pipeline 或批量命令:

pipe = r.pipeline(transaction=False) for gid in goods_ids: pipe.hmget(f"goods:info:{gid}", "title", "price", "brand", "category_id", "status") infos = pipe.execute()

一次网络往返拿到所有商品详情,性能提升非常明显。如果对耗时更敏感,可以把整个“分词 + 交集 + 过滤 + 取 ID”的逻辑写进 Lua 脚本,一次请求提交给 Redis 执行,避免多次网络往返。不过 Lua 脚本里面不要做太复杂的聚合,控制在几十毫秒内完成,否则同样会阻塞 Redis 单线程。

另外,对热点搜索词的首屏结果,我会在应用进程内做一层短缓存,比如 5 到 10 秒的过期时间,命中就直接返回,连 Redis 都不需要打。这一层缓存对高并发尖峰流量的削峰效果显著,代价是极小概率的数据短时不一致,商品搜索这种场景完全能接受。

4. 缓存治理与 Redis 实例的高可用保障

4.1 结果缓存与缓存穿透处理

搜索结果的缓存 key 设计为search:cache:{query_hash},value 是当前页商品 ID 列表和总数,TTL 设置在 30 到 120 秒之间。这个缓存能扛住绝大多数重复搜索请求,但要注意两个问题。

第一个问题是分页缓存一致性。如果只缓存第一页,用户翻到第三页时第一页缓存过期,可能导致上下页商品重复。我的做法是把缓存粒度定在“整个搜索条件和排序方式”上,TTL 统一较短,前几页一起缓存,翻页时整体刷新。这样虽然缓存命中率略降,但用户体验更稳。

第二个问题是缓存穿透。用户搜索一个从来没有商品关联的词,比如“哈哈哈哈”,索引查不到任何结果。如果不做处理,每次请求都穿透缓存打到索引,高并发下 Redis 也会被问出压力。处理手段是两个:一是把空结果也缓存起来,TTL 60 秒,避免同一个无效词反复打索引;二是在查询前加一个布隆过滤器,判断该词是否可能存在于索引中,不存在直接返回空。布隆过滤器用 String 位图自己实现就行,把词做三次哈希映射到位图,误判率控制在 1% 以内,内存消耗很小。

4.2 缓存击穿与分布式锁

缓存击穿发生在缓存过期瞬间,一个关键词的请求全部涌到索引层,瞬间产生大量重复查询。这时候需要一个分布式锁,让只有一个请求去重建缓存,其他请求等待或直接读旧数据。

锁的实现很简单,用 Redis 的SET key value NX EX命令:

lock_key = f"lock:search:{query_hash}" ok = r.set(lock_key, token, nx=True, ex=3) if ok: try: data = search_from_index(query) r.setex(cache_key, 60, data) finally: # 校验 token 后删除,避免误删别人的锁 lua = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """ r.eval(lua, 1, lock_key, token) else: time.sleep(0.05) data = r.get(cache_key) if not data: # 锁竞争失败且缓存还没重建,可以先返回默认搜索结果 pass

锁的 TTL 不能太短,否则在高延迟场景下锁提前释放,多个请求同时重建;也不能太长,否则持有锁的请求崩溃后锁要等超时才能释放。我一般设置 3 到 5 秒,索引构建逻辑控制在 1 秒内完成。释放锁时用 Lua 脚本校验 token,防止误删其他请求的锁,这个细节在并发量大的时候非常关键。

4.3 主从、持久化与独立实例规划

既然索引数据可以重算,为什么还要重视持久化和高可用?因为重建一套完整索引需要从数据库拉全量商品,可能耗时几十分钟,这段时间内搜索功能是不可用的。所以 RDB 和 AOF 该开还是要开,目标是“快速恢复而不是零丢失”。我建议 RDB 每 5 分钟生成一次,AOF 配置 everysec,恢复时优先用 RDB 缩短启动时间,AOF 做增量补齐。

主从结构要搭,但要注意读写分离的代价。从库可以分担一部分缓存读取和搜索结果读取,但主从复制存在延迟,刚上架的商品可能在从库上搜不到。我的处理是:搜索索引的读取尽量走主库,因为索引本身体量不大,主库单机足以支撑;从库只承担备份和容灾切换职责。如果业务对秒级延迟完全不敏感,可以放一部分读流量到从库,减轻主库压力。

最容易被忽视的一点:不要把搜索索引、业务缓存、分布式锁全部塞进同一个 Redis 实例。缓存实例的淘汰策略通常是 LRU 或 LFU,一旦内存紧张,索引数据可能被当成冷数据淘汰掉,搜索功能直接异常。我维护这套系统时单独给搜索索引分了一个实例,maxmemory 设成索引预估值的 1.5 倍,淘汰策略用 noeviction,锁和缓存放到另一个实例上,各司其职,互不干扰。

4.4 数据一致性与索引重建

商品信息变更到索引生效,本质上是一个异步过程。我推荐的做法是:商品变更写入数据库后,发送一条变更消息到队列,消费者拿到商品 ID 后重新计算所有分词和权重,先删旧词条索引再写新词条索引,最后更新 Hash 详情。消息失败要支持重试,重试超过三次进入死信队列人工处理。整个链路的延迟目标控制在秒级,用户在编辑后台改完商品标题,两三秒后前端可搜到新词,这个体验是可以接受的。

全量索引重建适合用双版本方案。比如要重建全量索引,先构建一批idx:word:v2:*的新 key,全部构建完成后,在配置中心把当前版本号从 v1 切到 v2,查询层读配置决定访问带哪个版本号的 key。这样避免直接覆盖生产索引时造成的长时间不可用,也保留了回滚能力。Redis 的 RENAME 命令虽然可以原子替换单个 key,但一个词条一个 key,搜索词涉及多个 key,没法靠一个命令完成整体切换,所以版本号才是稳妥做法。

5. 线上踩坑记录与优化实录

5.1 大词交集阻塞问题

上线后第一次压测,发现一个热门词查询时 Redis 整体延迟飙到 300 毫秒,其他命令全部排队。排查下来是这个词在倒排索引里有 8 万多个商品,和另一个大词用ZINTERSTORE做服务端聚合,单条命令执行耗时超过 200 毫秒,期间 Redis 单线程被占住,其他请求全部卡住。

修复方案有两个层次。第一层是把热门词条截断到 5000 个商品以内,从源头控制集合大小;第二层是超过截断阈值的词直接改用客户端聚合方式,把词条按ZRANGEBYSCORE分批取回,应用层用哈希集合求交集。这个调整之后,最坏情况下的单次查询耗时也控制在 50 毫秒以内。

5.2 过滤导致排序错乱

另一个引擎级问题:加了类目过滤后,返回结果排序完全乱了。排查发现ZINTERSTORE默认的聚合方式是 SUM,两个 ZSet 的 score 相加后,原来排序值 90 的商品加上过滤 ZSet 的 score 0,变成了一个奇怪的数。虽然看起来还是倒序,但和纯搜索排序结果不一致,用户明显感知到“这个商品为什么排前面”。

修复方法就是前面提到的:过滤维度的 ZSet score 全部置 0,ZINTERSTORE时指定AGGREGATE MAX,这样最终 score 完全来自搜索词条的排序值。这个坑非常隐蔽,如果只看命令手册不注意聚合行为,很难想到排序会被过滤操作改掉。

5.3 旧词残留与数据漂移

有段时间运营反馈,某个商品标题已经改成“夏季冰丝短裤”,但搜索“春季长裤”还是能搜到它。查了索引数据,发现这个商品有两个旧词“春季”和“长裤”的倒排条目残留。原因是商品变更流程里只处理了新词写入,没有先取旧词做删除。后来我把变更流程改成:先根据数据库里旧标题分词,得到旧词列表并删除;再基于新标题分词写入索引。同时加了一个离线巡检脚本,定期抽样商品,重新分词比对索引,自动清理异常条目。

5.4 缓存穿透风暴处置

有一回凌晨流量异常,某无良脚本疯狂请求大量不存在的搜索词,Redis 的查询量暴涨。由于当时没有布隆过滤器,所有无效请求都打到了索引层,虽然索引查询本身不算慢,但整体负载翻了好几倍。

处置分三步:第一步,把空结果缓存 TTL 调大到 5 分钟,立刻把重复无效请求挡掉;第二步,紧急上线了基于 String 位图的布隆过滤器,查询前先判断词是否存在;第三步,在网关层对单 IP 的搜索 QPS 做了限流。这套组合拳打完,Redis 负载回到正常水平。布隆过滤器误判会导致少量无效词穿透到索引层,但配合空结果缓存,已经足够兜住大部分异常流量。

5.5 注意事项速查表

场景推荐方案禁忌
分词召回不全词库切分 + 二元切分 + 英文小写规范化只依赖通用词库不做类目词扩充
大词索引膨胀每个词条截断保留前 5000 个商品不设上限导致 bigkey 和阻塞
多关键词交集小集合客户端聚合,大集合服务端聚合加截断对大集合大规模使用 ZINTERSTORE
过滤排序过滤 ZSet score 置 0,AGGREGATE MAX使用默认 SUM 导致排序错乱
缓存穿透布隆过滤器 + 空结果缓存不做防护让无效词直接打索引
缓存击穿Redis 分布式锁 + Lua 原子释放锁不设 TTL 或释放时不校验 token
实例规划搜索索引独立实例,noeviction和业务缓存混用被 LRU 淘汰
索引重建版本号双写切换直接覆盖生产 key 导致长时不可用
深分页限制 offset 上限放任深 offset 拖垮性能
商品变更异步队列 + 先删旧词再写新词只写新词不删旧词导致数据漂移

我在实际维护这套搜索模块的过程中,最大的体会是:Redis 搜索方案拼的不是 Redis 本身,而是对商品数据特征的把握。分词、权重、过滤维度这些业务细节拆得越清楚,索引结构就越简单,性能自然也就稳了。最后再分享一个小技巧,排查搜索排序问题时,直接在客户端里对某个词执行ZREVRANGE idx:word:{词} 0 20 WITHSCORES,看 score 的分布就能判断权重公式有没有生效。如果某个类目的商品 score 出现明显断层,调整归一化区间通常比改索引结构更省力。这套方案如果后续商品量真涨到了千万级,再做分片或者迁移到专业检索引擎,索引结构也能平滑过度,不会白做。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 17:35:45

Java后端数据传输与转换:从DTO到数据库的完整链路

Java 数据传输与转换详解&#xff1a;从 DTO 到数据库的完整链路做 Java 后端这几年&#xff0c;我最大的感受是&#xff1a;真正让项目出问题的&#xff0c;往往不是那些花哨的高并发方案&#xff0c;而是每天都要碰无数次的数据传输与数据转换。从 Controller 接收 JSON&…

作者头像 李华
网站建设 2026/10/9 17:35:05

从零搭建个人网站:云服务器、Nginx与HTTPS全流程实战

1. 个人网站搭建的整体设计与思路拆解1.1 为什么选择云服务器而不是虚拟主机或建站平台很多人第一次动念做个人网站&#xff0c;第一反应是去找那种“一键建站”的平台&#xff0c;拖拖拽拽就能出一个页面。但用过一段时间就会发现&#xff0c;免费套餐限制多、自定义能力弱、数…

作者头像 李华
网站建设 2026/10/9 17:34:54

S7-1500在汽车焊装自动线的应用与调试经验

从S7-300换到西门子1500PLC&#xff0c;是我做汽车焊装自动生产线这些年最明显的一次技术迭代。白车身焊装线上新线和老线改造&#xff0c;控制层基本绕不开1500&#xff0c;主焊线、地板线、侧围线、门盖线&#xff0c;十几台甚至几十台焊接机器人配上中频点焊机、滑橇输送、E…

作者头像 李华
网站建设 2026/10/9 17:34:52

Agent-Reach:面向AI开发者的智能体调用中枢与API编排引擎

1. 项目概述&#xff1a;Agent-Reach 是什么&#xff1f;它解决的不是“能不能用”&#xff0c;而是“怎么用得稳、用得准、用得省”Agent-Reach 这个名字乍看像某个开源模型或框架&#xff0c;但结合 CLI、API、YouTube、Reddit 等高频热词&#xff0c;以及大量围绕 codex cli…

作者头像 李华
网站建设 2026/10/9 17:31:19

JSP+Servlet手写成绩管理系统:从MVC架构到数据库设计与部署避坑指南

简介&#xff1a;一套基于JSP与Servlet的Web成绩管理系统课程设计源码&#xff0c;面向计算机专业学生与Java Web初学者&#xff0c;覆盖学生、教师、管理员三类角色的完整业务闭环。学生端支持个人信息维护与成绩查询&#xff0c;教师端提供成绩录入、修改、查看与分析&#x…

作者头像 李华