Redis Modules:RedisSearch、RedisJSON 与 RedisTimeSeries
第29章 Redis Modules:RedisSearch、RedisJSON 与 RedisTimeSeries
29.1 Module 机制概述
Redis Module API(Redis 4.0 引入)允许开发者以共享库(.so)形式扩展 Redis,注册新命令和新数据类型,访问 Redis 内核的各种能力(keyspace、事件、定时器、I/O),同时作为 Redis 进程的一部分运行,享受与内置命令相同的性能。
29.1.1 加载与管理
# 静态加载(redis.conf)
loadmodule /usr/lib/redis/modules/redisearch.so
loadmodule /usr/lib/redis/modules/rejson.so
loadmodule /usr/lib/redis/modules/redistimeseries.so
loadmodule /usr/lib/redis/modules/redisbloom.so
# 运行时加载
MODULE LOAD /path/to/module.so [arg1 arg2 ...]
# 查看已加载模块
MODULE LIST
# 1) 1) "name" 2) "search"
# 3) "ver" 4) (integer) 20601
# 5) "path" 6) "/usr/lib/redis/modules/redisearch.so"
# 7) "args" 8) (empty array)
# 卸载(注意:若有该模块的key存在,可能失败)
MODULE UNLOAD search
29.1.2 Module API 核心能力
- 注册命令:
RedisModule_CreateCommand,与内置命令平等竞争 - 注册数据类型:自定义序列化/反序列化(RDB/AOF 兼容)
- 访问 keyspace:读写 key,支持 TTL/过期
- 阻塞调用:
RedisModule_BlockClient,实现类 BLPOP 的阻塞命令 - 后台线程:
RedisModule_CreateTimer,异步处理 - 过滤器:命令前/后拦截(Command Filter API)
29.2 RedisSearch:全文搜索 + 向量搜索
RedisSearch 是 Redis Stack 的核心模块,提供倒排索引、聚合查询和向量相似度搜索能力。
29.2.1 索引创建
# 对 Hash 类型建索引
FT.CREATE product_idx
ON HASH
PREFIX 1 product:
SCHEMA
name TEXT WEIGHT 5.0 NOSTEM
description TEXT WEIGHT 1.0
price NUMERIC SORTABLE
category TAG SEPARATOR ","
stock NUMERIC
embedding VECTOR HNSW 6
TYPE FLOAT32
DIM 128
DISTANCE_METRIC COSINE
# 对 JSON 类型建索引
FT.CREATE user_idx
ON JSON
PREFIX 1 user:
SCHEMA
$.name AS name TEXT
$.age AS age NUMERIC SORTABLE
$.tags[*] AS tags TAG
$.bio AS bio TEXT
# 查看索引信息
FT.INFO product_idx
29.2.2 搜索查询语法
# 全文搜索
FT.SEARCH product_idx "redis database"
# 字段过滤
FT.SEARCH product_idx "@category:{database} @price:[10 100]"
# 布尔逻辑
FT.SEARCH product_idx "(redis | memcached) @price:[0 50]"
# 排序 + 分页
FT.SEARCH product_idx "cache"
SORTBY price ASC
LIMIT 0 20 # 从第0条开始,返回20条
# 返回指定字段
FT.SEARCH product_idx "redis"
RETURN 3 name price category
# 高亮匹配词
FT.SEARCH product_idx "fast cache"
HIGHLIGHT FIELDS 1 description
TAGS "<b>" "</b>"
# 模糊搜索(编辑距离 ≤ 1)
FT.SEARCH product_idx "%reddis%"
29.2.3 倒排索引原理
RedisSearch 为 TEXT 字段构建倒排索引:
原始文档:
product:1 → name="Redis Database Guide"
product:2 → name="Redis Performance Tips"
product:3 → name="PostgreSQL Guide"
倒排索引:
"redis" → [(product:1, TF=1/3), (product:2, TF=1/3)]
"database" → [(product:1, TF=1/3), (product:3, TF=1/3)]
"guide" → [(product:1, TF=1/3), (product:3, TF=1/3)]
"perform" → [(product:2, TF=1/3)] ← 词干化后
查询 "redis guide":
"redis" posting list ∩ "guide" posting list → product:1
BM25 打分:product:1 (score=2.5), product:3 (score=0.8)
中文分词支持:需要安装 friso 或 jieba 分词插件,或使用 Redis Stack 内置的中文分词器:
FT.CREATE cn_idx ON HASH PREFIX 1 doc:
SCHEMA content TEXT LANGUAGE chinese
29.2.4 聚合查询
# 按 category 聚合,统计每类商品数量和平均价格
FT.AGGREGATE product_idx "*"
GROUPBY 1 @category
REDUCE COUNT 0 AS count
REDUCE AVG 1 @price AS avg_price
SORTBY 2 @count DESC
# 返回示例:
# 1) "category" "database" "count" "45" "avg_price" "35.6"
# 2) "category" "cache" "count" "12" "avg_price" "20.1"
29.2.5 向量搜索(KNN)
向量搜索是 RedisSearch 2.4+ 的重量级特性,支持语义搜索和相似推荐:
索引类型对比:
| 索引类型 | 算法 | 精度 | 速度 | 内存 | 适用场景 |
|---|---|---|---|---|---|
| FLAT | 暴力全量扫描 | 100%(精确) | 随数据量线性降低 | O(N×DIM) | 数据量 < 100万 |
| HNSW | 分层可导航小世界图 | ~95%(近似) | O(log N) | O(N×DIM×M) | 数据量 > 100万 |
# FLAT 向量索引
FT.CREATE img_idx ON HASH PREFIX 1 img:
SCHEMA
embedding VECTOR FLAT 6
TYPE FLOAT32
DIM 512
DISTANCE_METRIC L2
# HNSW 向量索引(推荐大规模场景)
FT.CREATE product_idx ON HASH PREFIX 1 product:
SCHEMA
embedding VECTOR HNSW 12
TYPE FLOAT32
DIM 128
DISTANCE_METRIC COSINE
M 16 # 每层最大邻居数(影响精度和内存)
EF_CONSTRUCTION 200 # 构建时搜索宽度(影响索引质量)
EF_RUNTIME 10 # 查询时搜索宽度(影响速度和精度)
# KNN 查询:找最相似的10个商品
import struct
query_vec = struct.pack('128f', *embedding_array)
FT.SEARCH product_idx
"*=>[KNN 10 @embedding $vec AS score]"
PARAMS 2 vec "\x00\x01\x02..."
RETURN 3 name score price
SORTBY score
DIALECT 2
Python 示例(语义搜索):
import redis
import numpy as np
import struct
r = redis.Redis()
def add_product(product_id: str, name: str, embedding: np.ndarray):
"""存储商品及其 embedding 向量"""
vec_bytes = embedding.astype(np.float32).tobytes()
r.hset(f"product:{product_id}", mapping={
"name": name,
"embedding": vec_bytes
})
def search_similar(query_embedding: np.ndarray, top_k: int = 10):
"""向量相似搜索"""
vec_bytes = query_embedding.astype(np.float32).tobytes()
results = r.ft("product_idx").search(
Query("*=>[KNN $k @embedding $vec AS score]")
.sort_by("score")
.paging(0, top_k)
.dialect(2),
query_params={"k": top_k, "vec": vec_bytes}
)
return [(doc.name, float(doc.score)) for doc in results.docs]
29.2.6 与 Elasticsearch 对比
| 维度 | RedisSearch | Elasticsearch |
|---|---|---|
| 存储介质 | 内存(可持久化) | 磁盘(含缓存) |
| 查询延迟 | < 1ms | 5–50ms |
| 数据容量 | 受内存限制(GB级) | TB 级 |
| 向量搜索 | 内置(HNSW) | 需要 8.x+,性能接近 |
| 中文分词 | 需要插件 | 内置(ik_analyzer) |
| 集群扩展 | Redis Cluster | 原生分片 |
| 管理工具 | redis-insight | Kibana |
| 适用场景 | 高频小数据量精确查询 | 日志分析、大数据全文检索 |
29.3 RedisJSON:原生 JSON 存储
29.3.1 为何不用 String 存 JSON
传统方案:SET user:1000 '{"name":"Alice","age":30,"tags":["admin","user"]}'
问题:
- 修改一个字段需要读取整个 JSON、反序列化、修改、序列化、写回(至少2次网络往返)
- 无法对 JSON 内部字段建索引
- 并发修改容易覆盖(需要 WATCH + 事务)
RedisJSON 解决了这些问题:字段级读写、JSONPath 查询、与 RedisSearch 集成建索引。
29.3.2 基础操作
# 存储 JSON
JSON.SET user:1000 $ '{"name":"Alice","age":30,"tags":["admin","user"],"address":{"city":"Beijing"}}'
# 读取整个文档
JSON.GET user:1000
# JSONPath 读取($ = 根节点)
JSON.GET user:1000 $.name → ["Alice"]
JSON.GET user:1000 $.address.city → ["Beijing"]
JSON.GET user:1000 $.tags[0] → ["admin"]
# 读取多个路径
JSON.GET user:1000 $.name $.age → {"$.name":["Alice"],"$.age":[30]}
# 修改字段
JSON.SET user:1000 $.age 31
JSON.SET user:1000 $.address.city "Shanghai"
# 数值操作
JSON.NUMINCRBY user:1000 $.age 1 → [32]
# 数组操作
JSON.ARRAPPEND user:1000 $.tags '"superuser"' → [3] (新长度)
JSON.ARRLEN user:1000 $.tags → [3]
JSON.ARRPOP user:1000 $.tags 0 → ["admin"] (弹出第0个)
JSON.ARRINSERT user:1000 $.tags 0 '"root"' # 在索引0插入
# 删除字段
JSON.DEL user:1000 $.address.city
# 检查类型
JSON.TYPE user:1000 $ → ["object"]
JSON.TYPE user:1000 $.tags → ["array"]
JSON.TYPE user:1000 $.age → ["integer"]
# 文档大小(字节数)
JSON.STRLEN user:1000 $.name → [5]
JSON.OBJLEN user:1000 $ → [4] (顶层字段数)
JSON.ARRLEN user:1000 $.tags
29.3.3 JSONPath 完整语法
$ # 根节点
$.field # 属性访问
$[0] # 数组第0个元素
$[-1] # 数组最后一个元素
$[0:3] # 数组切片(索引0,1,2)
$.* # 所有直接子元素
$..field # 递归查找所有名为field的属性
$[?(@.age > 18)] # 过滤:age > 18 的元素
$[?(@.role == "admin")] # 过滤:role等于admin的元素
# 示例
JSON.GET users:list $[?(@.age >= 18)].name # 所有成年用户的名字
JSON.GET config $..timeout # 递归查找所有timeout字段
29.3.4 与 RedisSearch 集成
# 对 JSON 文档的字段建索引
FT.CREATE user_idx ON JSON PREFIX 1 user:
SCHEMA
$.name AS name TEXT
$.age AS age NUMERIC SORTABLE
$.tags[*] AS tags TAG
# 搜索
FT.SEARCH user_idx "@age:[18 30] @tags:{admin}"
RETURN 2 $.name $.age
29.3.5 内部实现
RedisJSON 不是简单地将 JSON 字符串存为 Redis String。它注册了自定义 Redis 数据类型,在内存中维护一棵 RapidJSON Document 树(基于 RapidJSON 库)。
优势:
- 字段级访问 O(depth),不需要完整序列化/反序列化
- JSONPath 查询在内存树上执行,比字符串解析快
- 可与 RedisSearch 深度集成,索引直接引用 JSON 字段
RDB 持久化:RedisJSON 实现了自定义 RDB 序列化(RedisModule_SaveString 族函数),JSON 树序列化为紧凑二进制格式存入 RDB。
29.4 RedisTimeSeries:时序数据存储
29.4.1 时序数据的特点与挑战
时序数据(Timeseries)是按时间戳顺序排列的数值序列,特点:
- 写入模式:追加写(append-only),几乎不更新历史数据
- 查询模式:时间范围查询、聚合降采样(downsampling)
- 数据量大:IoT、监控指标每秒可产生百万数据点
传统方案(Sorted Set)的问题:
ZADD sensor:temp $(date +%s%3N) 23.5每个数据点一个成员,内存开销大- 没有内置的时间聚合(平均、最大、最小等)
- 没有数据压缩
29.4.2 基础操作
# 创建时序 key
TS.CREATE temperature
RETENTION 86400000 # 数据保留时间(毫秒),0=永久
CHUNK_SIZE 4096 # 每个内存块大小(字节),默认 4096
ENCODING COMPRESSED # 压缩存储(默认),UNCOMPRESSED=不压缩
DUPLICATE_POLICY LAST # 重复时间戳处理策略
LABELS sensor_id 1 location "floor-3" unit "celsius"
# 添加数据点(* = 当前时间戳,单位毫秒)
TS.ADD temperature * 23.5
TS.ADD temperature 1716000000000 24.1 # 指定时间戳
# 批量添加(多个 key)
TS.MADD
temperature 1716000001000 23.8
humidity 1716000001000 65.2
pressure 1716000001000 1013.2
# 查询时间范围
TS.RANGE temperature - + # 全部数据(- = 最早,+ = 最新)
TS.RANGE temperature 1716000000000 1716003600000 # 指定范围(1小时)
# 带聚合的范围查询(每分钟平均值)
TS.RANGE temperature - +
AGGREGATION avg 60000 # 每60000毫秒(1分钟)聚合一次
# 其他聚合函数
# avg, sum, min, max, range, count, first, last, std.p, std.s, var.p, var.s
# 获取最新值
TS.GET temperature
# 反向查询(最新到最旧)
TS.REVRANGE temperature - + LIMIT 0 100
29.4.3 自动降采样(Compaction Rules)
# 创建降采样规则:每1分钟原始数据 → 每1小时平均值
TS.CREATERULE
temperature # 源 key
temperature:hourly_avg # 目标 key(需要先创建)
AGGREGATION avg 3600000 # 每3600000ms聚合一次
# 先创建目标 key
TS.CREATE temperature:hourly_avg RETENTION 2592000000 # 保留30天
# 查询降采样后的小时平均值
TS.RANGE temperature:hourly_avg - +
29.4.4 多 key 查询(MRANGE)
# 查询所有 sensor_id 标签的时序 key
TS.MRANGE - + FILTER sensor_id=1
TS.MRANGE - + AGGREGATION avg 60000 FILTER location="floor-3"
# 给 key 加标签,后续可以按标签筛选
TS.ALTER temperature LABELS sensor_id 1 location "floor-3" unit "celsius"
29.4.5 压缩算法详解
RedisTimeSeries 使用两级压缩减少内存占用:
时间戳压缩(Delta-of-Delta):
原始时间戳:1716000000, 1716000060, 1716000120, 1716000180
Delta(差值):60, 60, 60
Delta-of-Delta:0, 0, 0 ← 极度可压缩(规律时序几乎为0)
浮点值压缩(Gorilla XOR 压缩):
原始值:23.5, 23.6, 23.4, 23.7
XOR相邻值:相同符号位和指数位可以用1-2 bit表示
压缩率:规律波动数据可压缩到 1/4
实测压缩效果:
- 规律间隔时序(每秒1个点):压缩后约 1.5 字节/点(未压缩 16 字节)
- 随机浮点时序:压缩后约 8 字节/点
29.5 RedisBloom:概率数据结构
29.5.1 布隆过滤器(BF)
布隆过滤器回答一个问题:"这个元素是否曾经被添加过?" 可能有误判(false positive),但不会漏报(false negative)。
# 创建(指定误判率和预期容量)
BF.RESERVE users_seen 0.001 10000000 # 误判率0.1%,预期1000万元素
# 如果不存在则自动创建(默认:误判率1%,容量100)
BF.ADD users_seen "user:12345" → 1(新添加)
BF.ADD users_seen "user:12345" → 0(已存在)
# 批量添加
BF.MADD users_seen "user:1" "user:2" "user:3"
# 查询
BF.EXISTS users_seen "user:12345" → 1(可能存在)
BF.EXISTS users_seen "user:99999" → 0(一定不存在)
# 批量查询
BF.MEXISTS users_seen "user:1" "user:2" "user:unknown"
# 查看信息
BF.INFO users_seen
# Capacity: 10000000
# Size: 13807669 ← 实际占用字节数
# Number of filters: 1
# Number of items inserted: 3
# Expansion rate: 2
29.5.2 可扩容布隆过滤器
标准布隆过滤器容量固定,满了之后误判率急剧上升。RedisBloom 的 BF.ADD 会在容量不足时自动扩容(创建新的过滤层),保持误判率稳定:
# BF.RESERVE 的 EXPANSION 参数控制每次扩容倍数(默认2倍)
BF.RESERVE scalable_bf 0.01 1000 EXPANSION 2
# 初始容量1000,满后扩展到2000,再满扩展到4000,以此类推
扩容代价:每次扩容后新元素写入新层,旧元素仍在旧层,查询需要检查所有层。
29.5.3 布谷鸟过滤器(CF)
布谷鸟过滤器(Cuckoo Filter)支持删除操作,这是标准布隆过滤器不支持的:
CF.RESERVE items_cf 1000000 # 预期容量
CF.ADD items_cf "item:123" → 1
CF.EXISTS items_cf "item:123" → 1
CF.DELETE items_cf "item:123" → 1 ← 布隆过滤器不支持删除!
CF.EXISTS items_cf "item:123" → 0
布谷鸟过滤器原理:使用两个哈希表和"驱逐"策略,允许删除而不破坏数据结构完整性。误判率和内存与布隆过滤器相当,但插入有最坏情况退化(满负载时插入可能失败)。
29.5.4 Count-Min Sketch(频率估计)
# 用于估计元素出现频率(如热词统计、IP访问计数)
CMS.INITBYDIM sketch_name 2000 5 # 宽度2000,深度5
CMS.INCRBY sketch_name "redis" 1
CMS.INCRBY sketch_name "redis" 1
CMS.QUERY sketch_name "redis" → 2 (近似计数,可能偏高)
29.5.5 HyperLogLog(基数估计)
HyperLogLog 是 Redis 内置的概率基数计数器(PFADD/PFCOUNT),约 12KB 内存估计任意大基数,标准误差 0.81%:
PFADD unique_visitors "user:1" "user:2" "user:3"
PFCOUNT unique_visitors → 3
# 合并多个 HyperLogLog
PFADD visitors_today "user:1" "user:4"
PFADD visitors_yesterday "user:2" "user:5"
PFMERGE total_visitors visitors_today visitors_yesterday
PFCOUNT total_visitors → 4(估计值)
29.6 生产环境 Module 使用指南
29.6.1 Redis Stack vs 独立安装
| 部署方式 | 内容 | 推荐场景 |
|---|---|---|
| Redis Stack | 预装 Search/JSON/TS/Bloom | 开发/测试,快速上手 |
| Redis Cloud | 托管服务,按需选模块 | SaaS 生产 |
| 独立安装 .so | 精确控制版本,按需加载 | 自建生产环境 |
# Docker 快速启动 Redis Stack
docker run -d \
--name redis-stack \
-p 6379:6379 \
-p 8001:8001 \ # RedisInsight UI
redis/redis-stack:latest
29.6.2 内存规划
Module 的内存开销远超原生数据结构:
| 场景 | 原生 Redis | 加 Module |
|---|---|---|
| 100万文档全文索引 | 100MB(原始数据) | +300~500MB(倒排索引) |
| 100万时序数据点 | 16MB(Sorted Set) | 1.5~8MB(TS,压缩后) |
| 1000万布隆过滤器 | N/A | ~12MB(误判率1%) |
29.6.3 监控 Module 状态
# RedisSearch 监控
FT.DEBUG DUMP_INVIDX product_idx name # 查看词 "name" 的倒排索引
FT._LIST # 列出所有索引
FT.INFO product_idx | grep -A1 "num_docs\|indexing"
# RedisTimeSeries 监控
TS.INFO temperature
# totalSamples: 86400
# memoryUsage: 131072
# 通用 Module 内存
MEMORY USAGE <module_key>
INFO modules
29.7 小结
Redis Modules 将 Redis 从一个缓存/数据结构服务器扩展为一个多功能数据平台:
- RedisSearch:在 Redis 内存中实现亚毫秒级全文搜索和向量相似度查询,适合高频小数据量场景
- RedisJSON:字段级 JSON 读写,与 RedisSearch 深度集成,替代"读改写"模式
- RedisTimeSeries:专为时序数据优化,内置压缩和聚合,比 Sorted Set 节省 80%+ 内存
- RedisBloom:概率数据结构,用极小内存解决大规模去重和频率统计问题
Module 的代价是内存放大和运维复杂度。选型时需要权衡:在 Redis 内存中完成计算(低延迟)vs 使用独立专业系统(更高扩展性)。