RediSearch 开发实战 之 向量检索

摘要

向量是什么?

  • 在现代人工智能系统中,文本、图片、音频、视频等非结构化数据并不能直接被计算机理解和比较。

  • 向量(Vector)是将这些复杂信息转换为数学可计算形式的核心载体,也是大模型检索、推荐、搜索、RAG、语义理解等能力的基础。

数学意义上的向量

  • 在数学中,向量是一个有序的数值集合,例如:

1
[0.12, -0.87, 1.45, 0.003, ...]
  • 每一个数字称为一个维度(dimension),整体可以看作是在高维空间中的一个点。

  • 如果一个向量有 1024 个数字,则它位于一个 1024 维空间中。

向量在 AI 中的含义

  • 在 AI 领域,向量并不是随意的数字,而是 对某个对象语义特征的数值化表达,例如:

对象 向量表达含义
一句话 语义含义、主题、情感、上下文关系
一张图片 纹理、颜色、形状、语义对象
一段音频 音色、节奏、频谱特征
一个用户 兴趣偏好、行为模式
  • 模型通过学习大量数据,将“相似的语义”映射到“空间中距离接近的向量”。

向量为什么可以比较相似度?

  • 向量之间可以通过距离函数进行比较,常见距离指标:

指标 含义 适用场景
Cosine Similarity 余弦相似度 文本语义、Embedding
Euclidean Distance 欧式距离 图像、空间特征
Dot Product 点积 推荐系统
L2 / L1 范数距离 通用计算
1
2
向量A ≈ 向量B  → 表示语义相似
向量A 距离 向量C 很远 → 表示语义差异大
  • 向量相似度计算代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.langchain4j.embeding;

import com.example.langchain4j.ModelUtil;
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import org.apache.lucene.util.VectorUtil;

/**
* 向量demo
* Created by hanqf on 2026/1/13 09:51.
*/


public class VectorDemo {
public static void main(String[] args) {
EmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
.apiKey(ModelUtil.DASHSCOPE_API_KEY)
.modelName("text-embedding-v3")
.build();

String question = "Redis 7 如何开启向量检索功能?";

String[] texts = {
"Redis 需要安装 RediSearch 模块才能支持向量索引和相似度搜索。",
"Redis 7.4 可以通过 MODULE LOAD 加载向量模块。",
"MySQL 如何建立索引提升查询性能?",
"Spring Boot 如何配置 RedisTemplate 连接集群?"
};

Response<Embedding> embed = embeddingModel.embed(question);
float[] q_vector = embed.content().vector();
System.out.println(embed.content().vectorAsList());
// 获取向量的维度
System.out.println(embed.content().vector().length);

for (String s : texts) {
Response<Embedding> embedded = embeddingModel.embed(s);
float[] vector = embedded.content().vector();
// 计算余弦相似度, 范围[0,1],越大表示越相似
final float cosine = VectorUtil.cosine(q_vector, vector);
System.out.println(cosine);
}
}
}

向量模型(Embedding Model)

  • 向量模型非常多,常用的向量模型可以参考向量模型

  • 向量模型,也称为 Embedding 模型,是一种:把原始数据映射为固定长度向量的深度学习模型。

  • 输入可以是:文本、图片、音频、视频、多模态组合

  • 输出是:float[] embedding

向量维度的意义

  • 常见维度范围:

模型类型 向量维度
Mini Embedding 256 ~ 384
通用文本模型 512 ~ 1024
高精度模型 1536 ~ 4096
  • 维度越高:表达能力越强、存储和计算成本越高、检索延迟越大

向量模型的典型应用

场景 作用
语义搜索 搜索语义相关内容
RAG 文档召回
推荐系统 用户兴趣匹配
聚类分析 自动分类
去重 内容相似性检测
多模态检索 文搜图 / 图搜文

向量数据库

为什么需要向量数据库?

  • 普通数据库擅长:等值查询、范围查询、排序、聚合,但对:“在百万向量中找最相似的10个” 效率极低。

  • 向量数据库专门解决:高维向量的相似度检索问题(ANN Search)。

  • 向量数据库的能力

能力 说明
向量存储 高维 float 向量
相似度索引 HNSW、IVF、PQ
KNN 查询 TopK 最近邻
混合检索 向量 + 结构化过滤
持久化 磁盘存储
分布式 横向扩展
实时写入 在线更新

常见向量索引算法

算法 全称 核心原理 查询性能 检索精度 内存占用 构建成本 适用场景 典型系统
HNSW Hierarchical Navigable Small World 分层小世界图结构,通过多层图实现快速导航 极快(毫秒级) 极高(接近精确搜索) 较高 中等 实时检索、高 QPS、低延迟系统 Redis, Milvus, Faiss
IVF Inverted File Index 先聚类分桶,再在桶内进行向量搜索 中等~高(可调) 中等~低 较高(需要训练聚类) 大规模离线检索、成本敏感场景 Faiss, Milvus
PQ Product Quantization 向量分块压缩,用码本近似原始向量 较低(有量化误差) 极低 高(训练码本) 海量数据、内存受限系统 Faiss, Milvus

主流向量数据库

  • 向量数据库非常多,常用的向量数据库可以参考向量数据库

  • 这里只列出一些主流的向量数据库

产品 特点
Milvus 专业向量数据库
Qdrant Rust 高性能
Weaviate Graph + Vector
Elasticsearch 向量扩展
Redis 内存级向量
PGVector PostgreSQL 插件
Faiss 本地库
  • 向量数据库在 RAG 架构中的位置

1
2
3
4
5
6
7
8
9
用户问题

Embedding 模型 → 向量

向量数据库 KNN 查询

召回相关文档片段

LLM 生成答案
  • 向量数据库的能力决定了如下指标:

指标 含义
检索精度 检索结果质量,召回准确率
检索延迟 检索耗时
存储空间 索引大小
索引构建时间 索引构建耗时
系统吞吐能力 系统处理能力

RediSearch 创建向量索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建 JSON 数据 示例
JSON.SET vector:1 $ '{"vector":[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0], "text":"This is a test vector"}'

JSON.SET vector:2 $ '{"vector":[0.2,0.1,0.4,0.3,0.6,0.5,0.8,0.7,1.0,0.9], "text":"Another vector"}'

# 创建索引示例
FT.CREATE vector_index ON JSON
PREFIX 1 vector:
SCHEMA
$.text AS text TEXT
$.vector AS vector VECTOR
HNSW 10
TYPE FLOAT32
DIM 10
DISTANCE_METRIC COSINE
M 16
EF_CONSTRUCTION 200
  • 创建索引的参数说明

参数 含义
VECTOR 向量字段
HNSW 一种索引算法,可以把查询复杂度降低到近似 O(log N),牺牲极少精度换取极高性能。
10 表示后面紧跟的 10 个参数是用于创建向量索引的参数
TYPE FLOAT32 向量数据类型
DIM 10 向量维度,一般由向量模型的输出维度决定,如:1024
COSINE 相似度度量
M 16 每个节点在 HNSW 图中允许建立的最大邻居连接数
每个向量最多和多少个其它向量建立边关系
在插入一个新向量时:
算法会搜索一批候选邻居,
从候选集合中选择最多 M 个最合适的邻居建立双向连接
EF_CONSTRUCTION 200 建索引时搜索宽度
在构建索引(插入向量)时,用于搜索候选邻居的动态候选队列大小
  • M的大小影响的维度

维度 M 增大 M 减小
查询召回率 ↑ 提升 ↓ 降低
查询延迟 ↑ 略增 ↓ 更快
内存占用 ↑ 明显增加 ↓ 减少
构建时间 ↑ 增加 ↓ 更快
图稳定性 ↑ 更稳健 ↓ 容易断裂
  • M常见取值经验

场景 推荐 M
小规模数据(<100万) 16 ~ 32
高召回要求(搜索、推荐) 32 ~ 64
内存敏感 8 ~ 16
向量维度很高(>1024) 24 ~ 48

Redis 官方示例一般使用 M = 16,属于平衡型配置。

  • EF_CONSTRUCTION 影响维度

维度 EF_CONSTRUCTION 增大 EF_CONSTRUCTION 减小
索引质量 ↑ 提升 ↓ 下降
构建时间 ↑ 明显变慢 ↓ 更快
构建 CPU ↑ 占用增加 ↓ 减少
查询性能 ↑ 更稳定 ↓ 容易退化
内存 ≈ 基本不变

⚠️ 注意:EF_CONSTRUCTION 只影响建索引阶段,不影响查询阶段的实时性能。

  • EF_CONSTRUCTION 常见取值经验

经验公式:EF_CONSTRUCTION ≥ 2 × M

M 推荐 EF_CONSTRUCTION
16 100 ~ 200
32 200 ~ 400
48 300 ~ 600

搜索向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 搜索示例: 🔍 查询一个向量,找 Top 2 个最相似向量,并返回 text 和相似度距离 score。
FT.SEARCH vector_index
"*=>[KNN 2 @vector $vec AS score]"
PARAMS 2 vec "\xCD\xCC\xCC\x3D\xCD\xCC\x4C\x3E\x9A\x99\x99\x3E\xCD\xCC\xCC\x3E\x00\x00\x00\x3F\x9A\x99\x19\x3F\x33\x33\x33\x3F\xCD\xCC\x4C\x3F\x66\x66\x66\x3F\x00\x00\x80\x3F"
SORTBY score
RETURN 2 text score
DIALECT 2
## 返回结果
1) (integer) 2
2) "vector:1"
3) 1) "score"
2) "0"
3) "text"
4) "This is a test vector"
4) "vector:2"
5) 1) "score"
2) "0.0129868984222"
3) "text"
4) "Another vector"
  • 查询表达式: *=>[KNN 2 @vector $vec AS score]

参数 含义
*=> 这是 RediSearch 向量检索语法,这里表示匹配所有文档。
向量搜索通常不需要文本过滤,所以直接使用 *。
KNN 执行最近邻搜索(K-Nearest Neighbors)
2 返回最相近的 2 条记录
@vector 索引中的向量字段名
$vec 查询向量参数(来自 PARAMS)
AS score 将“距离结果”保存到字段名 score
  • PARAMS 参数绑定

1
2
3
4
5
# 语法
PARAMS <N> <key1> <value1> ... <keyN> <valueN>
# 本示例中表示:定义一个参数,参数名:vec,参数值:一个 float32 二进制向量
PARAMS 2 vec "<binary vector>"
# 注意这里要将 float 数组转换为FLOAT32二进制数据,不能直接传 [0.1,0.2,0.3]

float数组转换为FLOAT32二进制数据的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.langchain4j;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class FloatToByte {
public static void main(String[] args) {
float[] vector = new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f};
byte[] bytes = toFloat32Bytes(vector);
System.out.println(toRedisHex(bytes));
}

public static byte[] toFloat32Bytes(float[] vector) {
ByteBuffer buffer = ByteBuffer
.allocate(vector.length * 4)
.order(ByteOrder.LITTLE_ENDIAN); // Redis 要求小端序

for (float v : vector) {
buffer.putFloat(v);
}
return buffer.array();
}

public static String toRedisHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 4);
for (byte b : bytes) {
sb.append("\\x");
sb.append(String.format("%02X", b & 0xFF));
}
return sb.toString();
}
}
  • SORTBY score 排序规则

1
2
3
4
5
# 按 score 字段排序(默认升序)。
SORTBY score
## 注意,COSINE 度量在 Redis 中返回的是“距离”,不是“相似度”
score = 距离,距离越小 → 越相似 → 排在前面
如果希望返回的相似度得分,则 (1 - score)
  • RETURN 2 text score: 返回字段控制

  • DIALECT 2: Dialect 版本,向量搜索语法:=>[KNN ...] 必须使用 Dialect ≥ 2,否则会报语法错误。

使用 Redis 向量搜索的示例(基于 LangChain4j)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.example.langchain4j.embeding;

import com.example.langchain4j.ModelUtil;
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.community.store.embedding.redis.RedisEmbeddingStore;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.UnifiedJedis;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class RedisVectorDemo {

// EmbeddingModel
private static final EmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
.apiKey(ModelUtil.DASHSCOPE_API_KEY)
.modelName("text-embedding-v3")
.build();

// Redis连接信息
private static final DefaultJedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder()
.user("admin")
.password("123456")
.build();
private static final HostAndPort hostAndPort = new HostAndPort("127.0.0.1", 6379);

// EmbeddingStore 这里是Redis向量数据库
private static final EmbeddingStore<TextSegment> embeddingStore = RedisEmbeddingStore.builder()
.unifiedJedis(new UnifiedJedis(hostAndPort, jedisClientConfig))
.dimension(1024) // 模型返回的维度
.indexName("redis-vector-index") // 索引名称,默认 embedding-index
.prefix("redis-vector-embedding:") // 索引前缀,默认 embedding:
.build();

public static void main(String[] args) {

// 读取 rag.txt 并插入到 Redis
insertDocumentsToRedis();

String question = "Redis 7 如何开启向量检索功能?";
search(question);

}

/**
* 搜索向量
* @param question
*/
public static void search(String question){
TextSegment textSegment = TextSegment.from(question);
Response<Embedding> embed = embeddingModel.embed(textSegment);
final Embedding queryEmbedding = embed.content();

EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(3)
.build();
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.search(embeddingSearchRequest).matches();
for (EmbeddingMatch<TextSegment> match : matches) {
System.out.println(match.embedded().text());
System.out.println(match.score());
}
}

/**
* 插入文档到 Redis,初始化数据
*/
public static void insertDocumentsToRedis() {
try {
String content = Files.readString(Paths.get("rag.txt"));

// 简单按段落分割,也可以使用专门的文本分割工具
String[] paragraphs = content.split("\n\n");

for (String paragraph : paragraphs) {
if (!paragraph.trim().isEmpty()) {
TextSegment textSegment = TextSegment.from(paragraph.trim());
Response<Embedding> embeddingResponse = embeddingModel.embed(textSegment);
Embedding embedding = embeddingResponse.content();

// 将向量和文本段添加到 Redis
embeddingStore.add(embedding, textSegment);
System.out.println("已添加段落到向量库: " + textSegment.text().substring(0, Math.min(50, textSegment.text().length())) + "...");
}
}

System.out.println("所有文档已成功插入到 Redis 向量库中");

} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
}
}
}
  • rag.txt 内容

1
2
3
4
5
6
7
8
问题:Redis 7 如何开启向量检索功能?
答案:需要安装并加载 RediSearch 模块,然后创建包含 VECTOR 字段的索引即可支持向量相似度搜索。

问题:Redis 中 STRING 和 HASH 的主要区别是什么?
答案:STRING 适合存储单值或序列化对象,HASH 适合存储结构化字段,支持单字段读写和节省内存。

问题:Spring Boot 如何配置 RedisTemplate 连接 Redis 集群?
答案:需要配置 cluster.nodes 节点列表,并使用 Lettuce 客户端自动发现和维护集群拓扑。