Redis 向量检索实战:RediSearch、HNSW 与 Embedding 管道集成

Redis 向量检索实战:RediSearch 与 Redis Stack、向量索引(HNSW/FLAT)、向量相似度搜索(余弦/内积/L2)、与 Embedding 管道集成(OpenAI/本地模型)、向量+标签过滤、实时推荐/去重应用、性能与规模、与其他向量库对比

大模型时代,“语义检索"成为刚需:把文本、图片、商品编码成向量(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 两种索引算法

维度FLATHNSW
原理暴力全量比对分层可导航小世界图
召回精度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-small1536通用、性价比高API 计费
OpenAI text-embedding-3-large3072高质量、大模型API 计费
BAAI/bge-m31024中英多语言、开源本地 GPU/CPU
sentence-transformers/all-MiniLM-L6-v2384轻量、演示本地

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 向量检索让"语义能力"长在了缓存基础设施上,核心要点回顾:

  1. 能力载体:Redis Stack 的 RediSearch 2.4+ 原生支持向量索引,MODULE LOAD 即可激活
  2. 索引选型:百万级用 HNSW(M/EF_CONSTRUCTION/EF_RUNTIME 三参数调优),小数据量用 FLAT 精确召回
  3. 度量匹配模型:OpenAI 用 COSINE、归一化向量用 INNER_PRODUCT、图像特征用 L2,选错度量直接掉精度
  4. 管道要解耦:Embedding 离线批量生成、增量写入,模型升级必须重建索引
  5. 混合过滤是杀手锏:标签/数值过滤 + KNN 一条查询完成,这是 Redis 相比纯向量库的优势
  6. 规模有边界:百万级、复用基础设施、需混合过滤时选 Redis;十亿级、多租户强隔离时选独立向量库

向量检索选型的本质仍是"就近复用":当你的数据已经住在 Redis 里,语义检索只是加一个索引的事。先把 Embedding 管道跑通,再评估是否需要独立向量库——这是成本最低、见效最快的演进路径。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 缓存一致性终极方案:双删、binlog 订阅与最终一致性架构
  2. 高级数据结构实战:Bitmap、HyperLogLog、GEO、布隆过滤器与 Stream
  3. 过期键淘汰与内存回收深入:expire cycle、驱逐策略与碎片整理