大模型时代,“语义检索"成为刚需:把文本、图片、商品编码成向量(Embedding),用向量相似度替代关键词匹配。传统 Redis 只存"精确值”,而 Redis Stack 的 RediSearch 模块(2.4+) 让 Redis 原生支持向量索引与相似度搜索——这意味着语义搜索、RAG 检索、实时去重可以在同一套 Redis 基础设施上完成,无需引入独立的向量数据库。
本文从 RediSearch 的向量能力出发,讲解 HNSW/FLAT 索引原理、余弦/内积/L2 三种距离度量、与 OpenAI 及本地模型的 Embedding 管道集成、向量 + 标签的混合过滤、实时推荐与去重应用,最后给出性能规模分析与向量库选型对比。
一、向量检索生态:RediSearch 与 Redis Stack
1.1 什么是 Redis Stack
Redis Stack 是把 Redis 官方维护的多个模块打包的发行版,其中与向量检索相关的是:
| 模块 | 能力 |
|---|---|
| RediSearch | 全文搜索 + 向量相似度检索(VSS) |
| RedisJSON | 原生 JSON 文档存储 |
| RedisBloom | 布隆过滤器等概率结构 |
| RedisTimeSeries | 时序数据 |
生产环境用官方
redis/redis-stack-server镜像即可同时获得上述能力。若已有 Redis 单机,也可通过MODULE LOAD /path/to/search.so动态加载 RediSearch,无需迁移数据。
1.2 向量检索的两个基本步骤
1. 写入: 数据 -> Embedding 模型 -> 向量(float32) -> HSET 到 Redis Hash
2. 查询: 查询文本 -> Embedding 模型 -> 向量 -> FT.SEARCH KNN 相似度 TopN
# 检查 RediSearch 是否可用
redis-cli FT._LIST
# (empty array) # 尚未建索引
redis-cli MODULE LIST | grep search
# 2) "name" 3) "search" 4) "ver" 5) "20808"
二、向量索引原理:HNSW 与 FLAT
2.1 两种索引算法
| 维度 | FLAT | HNSW |
|---|---|---|
| 原理 | 暴力全量比对 | 分层可导航小世界图 |
| 召回精度 | 100% 精确 | 近似(可调 ef) |
| 查询延迟 | O(N) | O(log N) |
| 建索引耗时 | 快 | 较慢(需构建图) |
| 内存 | 与数据量成正比 | 略高于数据量 |
| 适用 | 数据量小(<1 万)、要求精确 | 百万级、追求低延迟 |
HNSW(Hierarchical Navigable Small World)是当前向量检索的主流算法:构建多层图结构,高层粗跳、底层细找。FLAT 是暴力扫描,数据量小或必须精确召回时才选它。
2.2 HNSW 核心参数
# FT.CREATE 时的 HNSW 参数
# VECTOR HNSW {参数个数} TYPE FLOAT32 DIM {维度} DISTANCE_METRIC {度量} M {边数} EF_CONSTRUCTION {建图} EF_RUNTIME {查询}
| 参数 | 作用 | 建议值 |
|---|---|---|
M | 每个节点的最大邻居数 | 16~64,越大召回越高、内存越大 |
EF_CONSTRUCTION | 建图时的候选集大小 | 100~400,越大图越优、建图越慢 |
EF_RUNTIME | 查询时的候选集大小 | 10~100,越大越精确、越慢 |
# 建索引示例:768 维、余弦距离、M=40
redis-cli FT.CREATE product_idx ON HASH PREFIX 1 product: SCHEMA \
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE \
name TEXT category TAG price NUMERIC
建图后
M与EF_CONSTRUCTION不可在线修改(需重建索引),EF_RUNTIME可在每次查询时用参数覆盖,实现"平时快、抽查准"。
三、向量相似度搜索:余弦、内积与 L2
3.1 三种距离度量
| 度量 | 公式语义 | 适用 | 注意 |
|---|---|---|---|
COSINE | 余弦相似度(1 - cos) | 文本语义检索 | 需向量归一化?RediSearch 内部处理 |
INNER_PRODUCT | 内积 | 已归一化向量、评分排序 | 值域无界,越大越相似 |
L2 | 欧氏距离 | 图像特征、几何距离 | 越小越相似 |
距离度量的选择取决于 Embedding 模型的约定:OpenAI 的
text-embedding-3官方建议余弦;训练时做了归一化的模型适合内积;CLIP 图像特征常用 L2。选错度量会显著降低召回质量。
3.2 建索引与 KNN 查询
# 1. 建索引(Hash 前缀 product:)
redis-cli FT.CREATE product_idx ON HASH PREFIX 1 product: SCHEMA \
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE \
name TEXT category TAG
# 2. 写入带向量的数据(向量是 float32 二进制,通常由程序写入)
# 程序侧: HSET product:1 embedding <1536*4字节> name "无线耳机" category electronics
# 3. 向量相似度 Top 10(DIALECT 2 必须开启)
redis-cli FT.SEARCH product_idx "*=>[KNN 10 @embedding $vec AS score]" \
PARAMS 2 vec "<查询向量二进制>" \
SORTBY score ASC DIALECT 2
# 4. 查看索引信息
redis-cli FT.INFO product_idx
3.3 Python 客户端完整示例
import numpy as np
from redis import Redis
from redis.commands.search.query import Query
r = Redis(host="localhost", port=6379, decode_responses=True)
# 建索引
from redis.commands.search.field import VectorField, TextField, TagField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
schema = [
TextField("name"),
TagField("category"),
VectorField("embedding", "HNSW", {
"TYPE": "FLOAT32", "DIM": 1536,
"DISTANCE_METRIC": "COSINE", "M": 40,
"EF_CONSTRUCTION": 200,
}),
]
r.ft("product_idx").create_index(
schema, definition=IndexDefinition(prefix=["product:"], index_type=IndexType.HASH))
# 写入向量(float32 little-endian 字节)
vec = np.random.rand(1536).astype(np.float32)
r.hset("product:1", mapping={"name": "无线耳机", "category": "electronics",
"embedding": vec.tobytes()})
# KNN 查询
q = Query("*=>[KNN 10 @embedding $vec AS score]") \
.params({"vec": query_vec.tobytes()}) \
.sort_by("score") \
.return_fields("name", "score") \
.dialect(2)
for doc in r.ft("product_idx").search(q).docs:
print(doc.name, doc.score)
decode_responses=True时注意:向量是二进制,查询的vec参数必须传 bytes 而非 str;return_fields记得带score才能取回相似度分数。
四、与 Embedding 管道集成:OpenAI 与本地模型
4.1 Embedding 模型对比
| 模型 | 维度 | 定位 | 成本 |
|---|---|---|---|
OpenAI text-embedding-3-small | 1536 | 通用、性价比高 | API 计费 |
OpenAI text-embedding-3-large | 3072 | 高质量、大模型 | API 计费 |
BAAI/bge-m3 | 1024 | 中英多语言、开源 | 本地 GPU/CPU |
sentence-transformers/all-MiniLM-L6-v2 | 384 | 轻量、演示 | 本地 |
4.2 OpenAI 管道
from openai import OpenAI
import numpy as np
client = OpenAI()
def embed_openai(text: str) -> bytes:
resp = client.embeddings.create(
model="text-embedding-3-small", input=text)
vec = np.array(resp.data[0].embedding, dtype=np.float32)
return vec.tobytes()
# 写入
r.hset("doc:1", mapping={"content": "Redis 向量检索实战",
"embedding": embed_openai("Redis 向量检索实战")})
# 查询
q_vec = embed_openai("如何使用 Redis 做语义搜索")
4.3 本地模型管道
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3") # 本地加载,离线可用
def embed_local(text: str) -> bytes:
vec = model.encode(text, normalize_embeddings=True)
return vec.astype(np.float32).tobytes()
4.4 管道设计要点
| 环节 | 要点 |
|---|---|
| 批量 Embedding | 离线批量生成向量,避免在线请求模型造成延迟 |
| 版本管理 | Embedding 模型升级会导致向量空间漂移,需重建索引 |
| 归一化 | 归一化后余弦 ≈ 内积,可换用 INNER_PRODUCT 提性能 |
| 增量写入 | 新数据实时 HSET,旧数据定期重算向量 |
生产建议:Embedding 管道与业务写入解耦——业务写业务字段,异步任务负责"文本 → 向量 → HSET"。模型升级时全量重建索引(
FT.DROPINDEX后重建),避免新旧向量混在同一个向量空间。
五、向量 + 标签过滤:混合检索
5.1 场景需求
单纯向量检索无法表达"品牌 + 价格区间 + 语义"的组合条件。RediSearch 允许把向量 KNN 与全文/标签/数值过滤写进同一条查询。
# 建索引:带 TAG(category)与 NUMERIC(price)字段
redis-cli FT.CREATE product_idx ON HASH PREFIX 1 product: SCHEMA \
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE \
name TEXT category TAG price NUMERIC brand TAG
# 混合查询:先按标签/数值过滤,再在结果内做向量相似度 Top 10
redis-cli FT.SEARCH product_idx \
"@category:{electronics} @price:[100 1000] =>[KNN 10 @embedding $vec AS score]" \
PARAMS 2 vec "<向量>" SORTBY score ASC DIALECT 2
5.2 过滤 + KNN 的执行语义
RediSearch 对"预过滤后 KNN"有两种处理:
| 模式 | 行为 | 适用 |
|---|---|---|
| 预过滤(pre-filter) | 先按 tag/numeric 过滤出候选集,再对候选集做向量搜索 | 过滤条件命中率低、候选集小 |
| 混合 | 向量搜索与过滤同时进行,结果取交集 | 通用 |
注意:当过滤条件过于严格(候选集很小)时,
KNN 10可能返回不足 10 条;反之候选集过大时预过滤会拖慢查询。生产上应结合LIMIT与DIALECT 3(新版混合执行更优)调优。
5.3 组合查询的工程示例
from redis.commands.search.query import Query
q = (Query("@category:{electronics} @price:[100 1000] "
"=>[KNN 10 @embedding $vec AS score]")
.params({"vec": q_vec})
.sort_by("score")
.return_fields("name", "price", "score")
.dialect(2))
for doc in r.ft("product_idx").search(q).docs:
print(doc.name, doc.price, doc.score)
六、实时推荐与去重应用
6.1 实时语义推荐
电商场景"看了这个商品还想看什么",用商品 Embedding 的向量相似度即可实现:
# 以商品 10086 的向量为查询向量,找相似商品 Top 10
redis-cli FT.SEARCH product_idx "*=>[KNN 10 @embedding $vec AS score]" \
PARAMS 2 vec "$(redis-cli HGET product:10086 embedding)" \
SORTBY score ASC DIALECT 2
# 在线推荐:读一次源商品向量,复用多次查询
src_vec = r.hget("product:10086", "embedding")
q = (Query("*=>[KNN 10 @embedding $vec AS score]")
.params({"vec": src_vec}).sort_by("score").dialect(2))
6.2 近重复检测与内容去重
UGC 场景(帖子、评论)用文本向量判断"几乎重复"的内容,避免垃圾灌水:
# 思路: 每条新内容 -> 向量 -> KNN 查询 -> 若 Top1 距离 < 阈值 判定重复
# 阈值经验值: COSINE 下 score < 0.15 视为高度相似(需按业务标定)
redis-cli FT.SEARCH post_idx "*=>[KNN 1 @embedding $vec AS score]" \
PARAMS 2 vec "<新帖向量>" SORTBY score ASC DIALECT 2
def is_duplicate(text: str, threshold: float = 0.15) -> bool:
vec = embed(text)
q = Query("*=>[KNN 1 @embedding $vec AS score]") \
.params({"vec": vec}).sort_by("score").dialect(2)
res = r.ft("post_idx").search(q)
return bool(res.docs) and float(res.docs[0].score) < threshold
6.3 应用场景一览
| 场景 | 做法 | 关键指标 |
|---|---|---|
| 语义搜索 | 文档 Embedding + KNN | 召回率、p99 延迟 |
| RAG 检索 | 知识库分块向量化 + TopK 上下文 | 检索准确率 |
| 实时推荐 | 物品 Embedding 相似 TopN | 点击率提升 |
| 近重复去重 | 距离阈值判定 | 误判率 |
| 图片相似 | CLIP 图像 Embedding + L2 | 检索精度 |
七、性能与规模
7.1 影响性能的因素
| 因素 | 影响 |
|---|---|
| 数据量 | HNSW 查询 O(log N),百万级 <10ms |
| 维度 | 维度越高内存越大、计算越慢 |
EF_RUNTIME | 与召回精度正相关、与延迟负相关 |
| 并发 | 单线程模块内计算,靠多分片扩展 |
| 过滤条件 | 预过滤候选集大小直接影响延迟 |
7.2 容量与内存估算
向量内存 ≈ 向量字节数 × 文档数 + HNSW 图开销(约 1.11.5 倍)。1536 维 float32 = 6KB/条,100 万条 ≈ 6GB 基础数据,加索引约 810GB。
# 观察向量索引内存
redis-cli FT.INFO product_idx | grep -E "indexing|hash_indexing_failures|total_indexing_time"
# 查看 Redis 内存,评估容量
redis-cli INFO memory | grep used_memory_human
7.3 压测方法
# 用 redis-benchmark 测 KNN 查询吞吐(DIALECT 2)
redis-benchmark -h 127.0.0.1 -p 6379 -n 10000 -c 50 \
-q -P 1 evalsha <搜索脚本> ...
# 更实用的是自写 Python 并发脚本测 p99 延迟
单机向量查询吞吐通常在每秒几千到几万次(取决于维度与 EF_RUNTIME)。要提升吞吐,横向扩容 Cluster 分片是最直接的手段——每个分片只承载部分数据。
八、与其他向量库对比
8.1 选型矩阵
| 方案 | 部署 | 强项 | 短板 |
|---|---|---|---|
| Redis Stack (RediSearch) | 内嵌 Redis | 复用缓存基础设施、混合过滤强 | 纯向量库功能相对简单 |
| FAISS | 应用内库 | 性能极致、灵活 | 需自建服务与持久化 |
| Milvus | 独立服务 | 分布式、超大规模、丰富索引 | 组件重、运维成本高 |
| Qdrant | 独立服务 | 过滤能力强、Rust 性能 | 需独立部署 |
| Pinecone | 托管 | 免运维、弹性 | 成本高、数据出网 |
| pgvector | 扩展 PostgreSQL | 与关系数据同库 | 大规模性能一般 |
8.2 什么时候选 Redis 向量检索
- 数据量在百万级以内,且已有 Redis 基础设施
- 需要向量 + 标签/数值混合过滤(RediSearch 的强项)
- 希望语义检索与现有缓存/会话/榜单共用一套存储
- 团队不想再引入并运维一个独立向量数据库
8.3 什么时候不选
- 十亿级向量、多租户隔离要求高 → Milvus / 托管服务
- 需要频繁批量更新且强一致 → 独立向量库更成熟
- 纯向量、无混合过滤需求 → FAISS 更轻
九、生产实践与监控
9.1 索引生命周期管理
# 重建索引流程(模型升级/维度变化时)
redis-cli FT.DROPINDEX product_idx # 删除索引(数据仍在)
# 重新 FT.CREATE 建新索引
redis-cli FT.CREATE product_idx ON HASH PREFIX 1 product: SCHEMA \
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE \
name TEXT category TAG price NUMERIC
# 全量重算向量并 HSET,或用 SCAN 遍历已有数据补充
9.2 监控指标
| 指标 | 获取方式 | 关注点 |
|---|---|---|
| 索引失败数 | FT.INFO 的 hash_indexing_failures | 有失败说明字段/类型不匹配 |
| 索引文档数 | FT.INFO 的 num_docs | 与预期数据量对比 |
| 查询延迟 | 业务侧埋点 | p99 应 <20ms |
| 内存 | INFO memory | 向量索引占比 |
9.3 最佳实践清单
- 向量维度与模型输出严格一致(1536/1024/384)
- 距离度量与模型约定一致(OpenAI 用 COSINE)
-
DIALECT 2(混合查询用 3)全局开启 - 模型升级走"重建索引"流程而非原地修改
- 向量字段与业务字段分 Key 管理,避免大 Key
- 为
FT.SEARCH设置TIMEOUT,防止慢查询拖累主链路
# 给查询设置超时(毫秒),避免拖累其他命令
redis-cli CONFIG SET search-timeout 500
结语
Redis 向量检索让"语义能力"长在了缓存基础设施上,核心要点回顾:
- 能力载体:Redis Stack 的 RediSearch 2.4+ 原生支持向量索引,
MODULE LOAD即可激活 - 索引选型:百万级用 HNSW(M/EF_CONSTRUCTION/EF_RUNTIME 三参数调优),小数据量用 FLAT 精确召回
- 度量匹配模型:OpenAI 用 COSINE、归一化向量用 INNER_PRODUCT、图像特征用 L2,选错度量直接掉精度
- 管道要解耦:Embedding 离线批量生成、增量写入,模型升级必须重建索引
- 混合过滤是杀手锏:标签/数值过滤 + KNN 一条查询完成,这是 Redis 相比纯向量库的优势
- 规模有边界:百万级、复用基础设施、需混合过滤时选 Redis;十亿级、多租户强隔离时选独立向量库
向量检索选型的本质仍是"就近复用":当你的数据已经住在 Redis 里,语义检索只是加一个索引的事。先把 Embedding 管道跑通,再评估是否需要独立向量库——这是成本最低、见效最快的演进路径。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。