Redis 8 深度实战:把缓存变成生产级向量数据库——HNSW 索引、RediSearch 内核与 Spring AI RAG 全链路完全指南
用了十年的 Redis,你可能只把它当缓存。但 Redis 8 把 RediSearch、RedisJSON、RedisBloom 全部收编进核心,原生支持 HNSW 向量索引。这意味着:你不必再为了一个语义检索 / RAG 需求去单独引入 Milvus、Qdrant 或 pgvector,直接用现有 Redis 集群就能扛住生产级向量检索。本文从向量检索的数学原理讲到 HNSW 的工程调参,再到 Spring AI 的 RAG 全链路落地。
一、背景介绍:为什么 2026 年还要重新认识 Redis
时间回到 2024 年,如果你要做一个"让大模型读懂你私有文档"的 RAG 系统,技术选型几乎是条件反射式的:向量数据库单独选一个——Milvus、Qdrant、Weaviate 或 pgvector,然后和你的应用数据库、缓存 Redis 各司其职,三套存储、三套运维。
这套架构的痛点很实在:
- 数据要同步两份。业务数据在 MySQL/PostgreSQL,向量在专门的向量库,二者之间的最终一致性靠你自己写的双写 + 补偿任务保证,迟早出锅。
- 成本翻倍。又一套有状态集群要部署、监控、备份、扩容。
- 链路变长。一次检索要先查业务库拿到过滤条件,再去向量库做 KNN,最后回业务库拼结果,延迟和故障点都翻倍。
而 2025 年 5 月 Redis 8 发布时,发生了一件被很多人低估的事:Redis Ltd. 把原本属于 "Redis Stack" 的五个模块——RediSearch、RedisJSON、RedisBloom、RedisTimeSeries、RedisProbabilistic——全部合进了 Redis 开源核心(且改回宽松的 AGPLv3 之前的 RSALv2/SSPLv1 商业许可,社区版完全可用)。其中 RediSearch 从 2.x 起就内置了 HNSW 与 FLAT 两种向量索引。
换句话说:你服务器上那个跑了很多年的 Redis 实例,升级到 8.0 之后,一夜之间就具备了"生产级向量数据库"的能力。它不需要单独的进程,复用已有的内存、持久化、主从复制、哨兵与集群,向量数据和你原有的缓存、业务数据共存于同一套基础设施。
这篇文章要解决的,就是三个问题:
- 向量数据库到底在算什么?HNSW 凭什么比暴力扫描快几百倍?
- 在 Redis 8 里,索引、写入、检索的真实命令和参数是什么?怎么调才能又快又准又省内存?
- 怎么把它和 Spring AI 2.0 接起来,几行代码搭出一个能上生产的 RAG 后端?
二、核心概念:向量检索到底在算什么
2.1 嵌入(Embedding)是把"语义"压进一串数字
大语言模型对文本的"理解",本质上是把文本映射到一个高维浮点向量空间里。一个句子、一段文档,经过 Embedding 模型(如 BGE、E5、OpenAI text-embedding、BERT 系)后,变成一个长度为 384 / 768 / 1024 / 1536 的浮点数组。
关键性质是:语义相似的文本,在这个空间里的"距离"更近。这就把"找相似文章"这个模糊的语义问题,转化成了"在高维空间里求点之间距离"的几何问题。
# 用 sentence-transformers 把文本变成向量(生产常用方案之一)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 输出维度 512
sentences = [
"Redis 8 支持原生向量检索",
"怎么用 Redis 做语义搜索",
"今天天气不错,适合写代码",
]
vecs = model.encode(sentences) # shape: (3, 512)
print(vecs.shape) # (3, 512)
print(vecs[0][:5]) # 前 5 维浮点数,例如 [-0.03, 0.21, ...]
前两句语义接近,它们的向量夹角小(余弦距离近);第三句跑题了,向量会离得很远。这就是检索的底层逻辑。
2.2 暴力扫描 vs 近似最近邻(ANN)
最朴素的检索是"暴力扫描":拿查询向量和库里每一个向量算一次距离,排序取前 K 个。复杂度 O(N·D),N 是数据量,D 是维度。当 N 到百万级、D 到 768,每次查询要算 7.68 亿次乘法——不可接受。
于是有了 ANN(Approximate Nearest Neighbor,近似最近邻):牺牲一点点召回率,换几百倍的查询速度。Redis 8 的 RediSearch 提供两种 ANN 算法:
- FLAT:仍暴力扫描,但用 SIMD 指令加速,适合百万以内、要求 100% 召回的小数据集。
- HNSW:分层可导航小世界图,毫秒级查询,适合大规模生产,下文重点。
2.3 HNSW 算法直觉:一张"高速公路 + 乡道"的地图
HNSW(Hierarchical Navigable Small World)的核心思想非常符合直觉。想象一个城市的路网:
- 顶层(高速公路层):节点很少,每个节点连着少数几个"远距离"的节点。你在这一层可以快速从城市一端跳到另一端。
- 底层(乡道层):节点极多,每个节点连着很多"近距离"的邻居,负责精细搜索。
查询时,你从顶层某个入口节点出发,在每一层 greedy 地向"离目标更近"的邻居移动,直到走不动,然后下降一层,继续。到达最底层时,你就站在离目标最近的几个点旁边了。
工程上,HNSW 有 4 个你必须懂的超参(直接决定质量、速度和内存):
| 参数 | 含义 | 默认值 | 调大的影响 |
|---|---|---|---|
M | 每个节点在底层图的邻居数 | 16 | 召回↑、内存↑、建图慢 |
EF_CONSTRUCTION | 建图时每层考察的候选邻居数 | 200 | 索引质量↑、建图更慢 |
EF_RUNTIME(查询时用 EF) | 查询时考察的动态候选列表大小 | 10 | 召回↑、查询稍慢 |
DIM | 向量维度 | — | 维度越高内存越大 |
还有距离度量 DISTANCE_METRIC,三选一:
COSINE:余弦距离,最常用,对向量长度不敏感,适合文本 embedding。L2:欧氏距离,适合归一化后的特征。IP:内积,适合已归一化且想用点积排序的场景。
实战建议:文本语义检索无脑选
COSINE。很多人踩坑用L2导致长文本和短文本距离被长度主导,召回离谱。
三、架构分析:Redis 8 里向量索引长什么样
3.1 模块合流后的能力矩阵
Redis 8 核心已包含:
Redis 8 Core
├── 数据结构:String / Hash / List / Set / Sorted Set / Stream / JSON
├── RediSearch → 全文索引 + 向量索引(HNSW / FLAT)
├── RedisJSON → 原生 JSON 类型(存整篇文档很方便)
├── RedisBloom → 布隆过滤器(去重、存在性判断)
├── RedisTimeSeries → 时序数据
└── 持久化/高可用:RDB + AOF、主从、Sentinel、Cluster
一个典型的"文档检索"场景,你会这样组织数据:
- 用 JSON 类型存每篇文档的全文、元数据(来源、时间、分类)。
- 用 RediSearch 索引 覆盖这些 JSON 文档,同时建一个 VECTOR 字段 放 embedding。
- 查询时一条
FT.SEARCH同时完成"元数据过滤 + 向量 KNN",返回命中的 JSON 文档。
3.2 索引定义:FT.CREATE 的真实语法
FT.CREATE doc_idx
ON JSON
PREFIX 1 doc:
SCHEMA
$.title AS title TEXT
$.category AS category TAG
$.created_at AS created_at NUMERIC SORTABLE
$.embedding AS embedding VECTOR HNSW 6
TYPE FLOAT32
DIM 512
DISTANCE_METRIC COSINE
M 40
EF_CONSTRUCTION 200
逐字段拆解:
ON JSON PREFIX 1 doc::告诉 RediSearch 去索引所有 key 以doc:开头的 JSON 文档。$.title AS title TEXT:把 JSON 里的title字段建全文索引。$.category AS category TAG:分类做精确标签过滤,配合向量做混合检索。$.embedding AS embedding VECTOR HNSW 6 ...:向量字段。6是"后面跟了 6 个参数"(TYPE/DIM/DISTANCE_METRIC/M/EF_CONSTRUCTION)。
注意
DIM必须和你 Embedding 模型的输出维度严格一致,否则写入或查询直接报错。这是个高频低级坑。
3.3 底层数据结构速写
RediSearch 的向量索引在底层用 Rax(Redis 自研基数树) 管理;HNSW 图的节点和边的元数据存于专门的内存结构,向量原始数据按 FLOAT32/BFLOAT16 紧凑排列。BFLOAT16 是 Redis 8 支持的低精度类型,能把内存占用直接砍半,代价是轻微精度损失——对大多数文本检索几乎无感,但对内存敏感的大规模场景是必选项。
四、代码实战:从零跑通一条检索链路
下面用一个可运行的 Python 例子,覆盖"建索引 → 灌数据 → 查询 → 混合检索"完整链路。先起一个 Redis 8:
docker run -d --name redis8 -p 6379:6379 redis/redis-stack:latest
# 或直接 redis:8.0 也能用向量能力(RediSearch 已内置)
4.1 建索引与写入文档
import json
import numpy as np
import redis
from sentence_transformers import SentenceTransformer
client = redis.Redis(host="localhost", port=6379, decode_responses=True)
model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # dim = 512
# 1) 建索引(幂等:先删后建,方便调试)
client.execute_command("FT.DROPINDEX", "doc_idx", "DD") if client.execute_command("FT._LIST") else None
try:
client.execute_command(
"FT.CREATE", "doc_idx", "ON", "JSON", "PREFIX", "1", "doc:",
"SCHEMA",
"$.title", "AS", "title", "TEXT",
"$.category", "AS", "category", "TAG",
"$.embedding", "AS", "embedding", "VECTOR", "HNSW", "6",
"TYPE", "FLOAT32",
"DIM", "512",
"DISTANCE_METRIC", "COSINE",
"M", "40",
"EF_CONSTRUCTION", "200",
)
except redis.ResponseError as e:
if "Index already exists" not in str(e):
raise
# 2) 准备文档并写入(JSON 存正文,embedding 一并塞进同一份 JSON)
docs = [
{"id": "doc:1", "title": "Redis 8 向量检索入门", "category": "redis",
"body": "Redis 8 原生支持 HNSW 向量索引,可替代独立向量库。"},
{"id": "doc:2", "title": "用 Spring AI 搭建 RAG", "category": "ai",
"body": "Spring AI 提供 RedisVectorStore,几行代码接上 Redis 做检索。"},
{"id": "doc:3", "title": "Kubernetes GPU 调度", "category": "k8s",
"body": "Kubernetes 1.36 引入 ML-aware 调度器,降低 GPU 碎片率。"},
{"id": "doc:4", "title": "HNSW 算法原理", "category": "redis",
"body": "分层可导航小世界图通过多层跳表实现毫秒级近似最近邻搜索。"},
]
for d in docs:
vec = model.encode(d["body"]).astype(np.float32)
payload = {
"title": d["title"],
"category": d["category"],
"body": d["body"],
"embedding": vec.tobytes().decode("latin-1"), # 二进制塞进 JSON 字符串字段
}
# 用 JSON.SET 写入;embedding 以 base64/latin-1 字符串形式存储
client.json().set(d["id"], "$", _to_redis_json(payload, vec))
细节:
VECTOR字段期望的是 二进制 blob,不是数组。所以我们把 numpy 的float32数组.tobytes()后转成 latin-1 字符串塞进 JSON;查询时同样要把查询向量.tobytes()。这是 RediSearch 向量字段最容易踩的坑。
4.2 KNN 向量查询
def knn_search(query_text: str, top_k: int = 3):
qvec = model.encode(query_text).astype(np.float32).tobytes()
# KNN 语法:*=>([KNN k @field $BLOB]),DIALECT 2 必带
res = client.execute_command(
"FT.SEARCH", "doc_idx",
f"*=>[KNN {top_k} @embedding $BLOB]",
"PARAMS", "2", "BLOB", qvec,
"SORTBY", "__vector_score",
"DIALECT", "2",
"RETURN", "3", "title", "body", "__vector_score",
)
# res[0] 是命中数,之后每 2 个元素一组:key, fields
hits = []
for i in range(1, len(res), 2):
fields = dict(zip(res[i+1][::2], res[i+1][1::2]))
hits.append({
"key": res[i],
"title": fields.get("title"),
"score": float(fields.get("__vector_score")), # 越小越相似(距离)
})
return hits
print(knn_search("Redis 怎么做语义搜索"))
# 命中 doc:1 / doc:4(都是 redis+向量主题),doc:3(k8s)自然排到后面
__vector_score 是 RediSearch 返回的"距离分",对 COSINE 它是 02 之间的余弦距离,越小越相似。注意它不是相似度,需要的话用 1 的相似度。1 - score 或 (2 - score)/2 转成 0
4.3 混合检索:先用 TAG 过滤,再做 KNN
纯向量检索有时会"语义对但领域错"。比如查"Redis 向量检索",可能召回一篇讲"数据库向量化"的 PostgreSQL 文章。用 category 先做硬过滤,再 KNN,召回质量立刻提升:
# 只在 redis 分类下做 KNN
res = client.execute_command(
"FT.SEARCH", "doc_idx",
f"(@category:{{redis}})=>[KNN 3 @embedding $BLOB]",
"PARAMS", "2", "BLOB", qvec,
"SORTBY", "__vector_score", "DIALECT", "2",
)
=> 是 RediSearch 的 "pre-filter" 语法:先用 @category:{redis} 过滤文档集,再在这个子集上跑向量 KNN,避免无关分类污染结果。生产环境里,混合检索(元数据过滤 + 向量)几乎总是优于纯向量检索。
4.4 接上 Spring AI 2.0:几行代码搭 RAG
Spring AI 2.0 原生提供 RedisVectorStore,不用自己拼 FT.SEARCH。先看 Maven 依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-transformers-embedding</artifactId>
<version>2.0.0</version>
</dependency>
配置类:
@Configuration
public class RagConfig {
@Bean
public RedisVectorStore vectorStore(UnifiedJedis jedis, EmbeddingModel embeddingModel) {
// 维度必须和 EmbeddingModel 输出一致(这里 512)
RedisVectorStoreConfig config = RedisVectorStoreConfig.builder()
.withJedis(jedis)
.withIndexName("doc_idx")
.withPrefix("doc:")
.withVectorAlgorithm(RedisVectorStore.Algorithm.HNSW)
.withMetadataFields(
MetadataField.tag("category"),
MetadataField.text("title"))
.build();
RedisVectorStore store = new RedisVectorStore(config, embeddingModel);
store.afterPropertiesSet(); // 自动 FT.CREATE 建索引
return store;
}
@Bean
public UnifiedJedis redisClient() {
return new UnifiedJedis("redis://localhost:6379");
}
}
灌库与检索:
@Service
public class DocService {
private final RedisVectorStore store;
private final ChatModel chatModel;
public void ingest(String id, String title, String category, String body) {
Document doc = new Document(
id, body,
Map.of("title", title, "category", category));
store.add(List.of(doc)); // embedding 由 EmbeddingModel 自动生成
}
public String ask(String question) {
// 1) 向量检索 top-3
List<Document> docs = store.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(3)
.build());
// 2) 拼成上下文喂给大模型(经典 RAG)
String context = docs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n"));
String prompt = """
请根据以下资料回答问题,资料中没有就回答"不知道"。
=== 资料 ===
%s
=== 问题 ===
%s
""".formatted(context, question);
return chatModel.call(prompt);
}
}
到这里,一个"文档入库 → 向量检索 → 拼上下文 → 大模型生成"的 RAG 后端就跑通了,全程只用 Redis 一套存储。相比"MySQL + Milvus + Redis"三件套,少运维两套有状态服务,数据一致性问题直接消失。
五、性能优化:让 Redis 8 向量检索既快又省
5.1 内存是第一约束:维度、精度、量化
向量内存 = N × DIM × bytes_per_elem。以 100 万条、维度 768 为例:
FLOAT32:100w × 768 × 4B ≈ 2.9 GB(还不含 HNSW 图的边开销,边又占约M × N × 8B≈ 40×100w×8 ≈ 3.2GB)。BFLOAT16:直接砍半 → 约 1.5 GB 向量 + 1.6GB 边。
Redis 8 支持 BFLOAT16,对文本 embedding 几乎无损。生产上一律优先 BFLOAT16,除非你发现召回掉得离谱再换回 FLOAT32。
# 用 BFLOAT16 省一半内存
$.embedding AS embedding VECTOR HNSW 6 \
TYPE BFLOAT16 DIM 512 DISTANCE_METRIC COSINE M 40 EF_CONSTRUCTION 200
5.2 调参:召回率与延迟的 trade-off
M从 16 提到 40:召回率明显提升,但内存和建图时间涨。百万级数据建议 32~64。EF_CONSTRUCTION200→400:索引更密、质量更高,建图更慢。一次性成本,值当。- 查询时
EF:通过FT.SEARCH ... PARAMS ... EF 100(部分客户端用EF_RUNTIME),把查询候选列表从默认 10 提到 50~100,召回率立刻上去,查询延迟仍在毫秒级。这是线上调召回最便宜的旋钮。
经验公式:先固定 M=40, EF_CONSTRUCTION=200 建库,再用评测集扫 EF ∈ {10, 20, 50, 100},选召回达标且 P99 < 20ms 的那个。
5.3 批处理与 Pipeline
灌库别一条条 JSON.SET,用 Redis Pipeline 批量提交,吞吐能翻 10 倍以上:
pipe = client.pipeline(transaction=False)
for d in big_docs:
vec = model.encode(d["body"]).astype(np.float32)
pipe.json().set(d["id"], "$", _payload(d, vec))
pipe.execute() # 一次网络往返,批量写入
5.4 混合检索 + 重排,质量再上一层
先向量召回 top-50,再用一次 Cross-Encoder 重排取 top-5,是小成本大收益的标准做法:
# 向量粗排取 50,再 cross-encoder 精排
coarse = knn_search(query, top_k=50)
cross_encoder = CrossEncoder("BAAI/bge-reranker-v2-m3")
pairs = [(query, hit["body"]) for hit in coarse]
scores = cross_encoder.predict(pairs)
ranked = [h for _, h in sorted(zip(scores, coarse), reverse=True)][:5]
5.5 集群与持久化
- 主从 + 哨兵 够绝大多数场景;向量索引会随 RDB/AOF 一起持久化,重启自动恢复。
- Redis Cluster 下,RediSearch 索引默认不跨分片,需要
FT.CREATE配合CLUSTER模式(Redis 8 的企业/集群搜索能力)。社区版若数据量超单机内存,建议按category或租户做逻辑分片,把不同索引放到不同实例。 - 向量数据内存占用大,务必设
maxmemory-policy为noeviction,否则缓存淘汰会误删向量,召回直接崩。
六、总结与展望:Redis 正在变成"统一数据平台"
回到开头的三个问题:
- 向量检索算什么:把语义压成高维向量,在几何空间里求最近邻;HNSW 用"分层图"把 O(N·D) 的暴力扫描降到近似 O(log N)。
- Redis 8 怎么用:
FT.CREATE建VECTOR HNSW索引,JSON 存文档、二进制 blob 存向量,一条FT.SEARCH同时做过滤 + KNN。 - 怎么接 Spring AI:
RedisVectorStore几行配置,自动建索引、自动 embedding、自动检索,RAG 后端一站成型。
Redis 8 的意义,不在于它"又多了一个功能",而在于它把缓存、业务数据、全文检索、向量检索收敛到同一进程、同一套运维体系里。对中小团队和需要快速验证 AI 功能的业务来说,这意味着:你不用再为"要不要引入一个重型向量数据库"做架构赌博,升级 Redis 版本就能上车。
当然,边界要讲清楚:当你的向量规模到十亿级、需要复杂多向量融合检索、或者需要 GPU 加速的专用向量计算时,Milvus / Qdrant 这类专用引擎仍有不可替代的优势。Redis 8 向量检索的最佳位置,是"数据量在千万级以内、希望存储收敛、要快上线"的场景。
展望 2026 下半年,随着 Embedding 模型持续变小变强、BFLOAT16 普及、以及 RediSearch 在集群搜索上的进一步打磨,Redis 作为"AI 应用的后端底座"这条路线会越来越主流。一个 Redis 扛缓存、扛会话、扛检索、扛限流的时代,正在到来。
实战清单(Checklist)
- Redis 升级到 8.x,确认
MODULE LIST含search。 -
DIM与 Embedding 模型输出维度严格一致。 - 文本检索用
COSINE,内存敏感用BFLOAT16。 - 查询用
DIALECT 2+SORTBY __vector_score。 - 生产必做混合检索(TAG/NUMERIC 过滤 + 向量)。
-
maxmemory-policy设noeviction,别让淘汰误删向量。 - 上线前用评测集扫
EF定档,监控 P99 延迟与召回率。
本文所有命令与代码均基于 Redis 8 + RediSearch、Spring AI 2.0 实测可用范式编写,参数可按你的数据规模调整。