第 29 章

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 核心能力


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)

中文分词支持:需要安装 frisojieba 分词插件,或使用 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"]}'

问题:

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 库)。

优势:

RDB 持久化:RedisJSON 实现了自定义 RDB 序列化(RedisModule_SaveString 族函数),JSON 树序列化为紧凑二进制格式存入 RDB。


29.4 RedisTimeSeries:时序数据存储

29.4.1 时序数据的特点与挑战

时序数据(Timeseries)是按时间戳顺序排列的数值序列,特点:

传统方案(Sorted Set)的问题:

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

实测压缩效果:


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 从一个缓存/数据结构服务器扩展为一个多功能数据平台:

Module 的代价是内存放大和运维复杂度。选型时需要权衡:在 Redis 内存中完成计算(低延迟)vs 使用独立专业系统(更高扩展性)。

本章评分
4.9  / 5  (3 评分)

💬 留言讨论