编程 Redis 8 深度实战:把缓存变成生产级向量数据库——HNSW 索引、RediSearch 内核与 Spring AI RAG 全链路完全指南

2026-07-12 05:41:10 +0800 CST views 22

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 各司其职,三套存储、三套运维。

这套架构的痛点很实在:

  1. 数据要同步两份。业务数据在 MySQL/PostgreSQL,向量在专门的向量库,二者之间的最终一致性靠你自己写的双写 + 补偿任务保证,迟早出锅。
  2. 成本翻倍。又一套有状态集群要部署、监控、备份、扩容。
  3. 链路变长。一次检索要先查业务库拿到过滤条件,再去向量库做 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 - score(2 - score)/2 转成 01 的相似度。

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_CONSTRUCTION 200→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-policynoeviction,否则缓存淘汰会误删向量,召回直接崩。

六、总结与展望:Redis 正在变成"统一数据平台"

回到开头的三个问题:

  1. 向量检索算什么:把语义压成高维向量,在几何空间里求最近邻;HNSW 用"分层图"把 O(N·D) 的暴力扫描降到近似 O(log N)。
  2. Redis 8 怎么用FT.CREATEVECTOR HNSW 索引,JSON 存文档、二进制 blob 存向量,一条 FT.SEARCH 同时做过滤 + KNN。
  3. 怎么接 Spring AIRedisVectorStore 几行配置,自动建索引、自动 embedding、自动检索,RAG 后端一站成型。

Redis 8 的意义,不在于它"又多了一个功能",而在于它把缓存、业务数据、全文检索、向量检索收敛到同一进程、同一套运维体系里。对中小团队和需要快速验证 AI 功能的业务来说,这意味着:你不用再为"要不要引入一个重型向量数据库"做架构赌博,升级 Redis 版本就能上车。

当然,边界要讲清楚:当你的向量规模到十亿级、需要复杂多向量融合检索、或者需要 GPU 加速的专用向量计算时,Milvus / Qdrant 这类专用引擎仍有不可替代的优势。Redis 8 向量检索的最佳位置,是"数据量在千万级以内、希望存储收敛、要快上线"的场景。

展望 2026 下半年,随着 Embedding 模型持续变小变强、BFLOAT16 普及、以及 RediSearch 在集群搜索上的进一步打磨,Redis 作为"AI 应用的后端底座"这条路线会越来越主流。一个 Redis 扛缓存、扛会话、扛检索、扛限流的时代,正在到来。


实战清单(Checklist)

  • Redis 升级到 8.x,确认 MODULE LISTsearch
  • DIM 与 Embedding 模型输出维度严格一致。
  • 文本检索用 COSINE,内存敏感用 BFLOAT16
  • 查询用 DIALECT 2 + SORTBY __vector_score
  • 生产必做混合检索(TAG/NUMERIC 过滤 + 向量)。
  • maxmemory-policynoeviction,别让淘汰误删向量。
  • 上线前用评测集扫 EF 定档,监控 P99 延迟与召回率。

本文所有命令与代码均基于 Redis 8 + RediSearch、Spring AI 2.0 实测可用范式编写,参数可按你的数据规模调整。

推荐文章

Vue3中的v-slot指令有什么改变?
2024-11-18 07:32:50 +0800 CST
联系我们
2024-11-19 02:17:12 +0800 CST
pin.gl是基于WebRTC的屏幕共享工具
2024-11-19 06:38:05 +0800 CST
Go 单元测试
2024-11-18 19:21:56 +0800 CST
mendeley2 一个Python管理文献的库
2024-11-19 02:56:20 +0800 CST
支付宝批量转账
2024-11-18 20:26:17 +0800 CST
一键压缩图片代码
2024-11-19 00:41:25 +0800 CST
程序员茄子在线接单