Redis Vector Sets
Vector Sets 是 Redis 8.0 引入的原生数据类型(Beta 版本),专门用于存储和检索高维向量数据,支持高效的向量相似性搜索。这一特性由 Redis 的创始人 Salvatore Sanfilippo(antirez)设计开发,使 Redis 能够原生支持 AI 应用中的向量检索需求,无需依赖外部向量数据库或额外的模块。
Vector Sets 在 Redis 8.0 中以 Beta 版本发布,API 和功能可能在未来版本中发生变化。对于需要企业级搜索能力(如全文搜索、混合查询)的场景,建议使用 Redis Query Engine。
概述
什么是 Vector Sets?
Vector Sets 类似于 Sorted Sets,但每个元素关联的是一个向量(而非分数)。Vector Sets 专门设计用于:
- 相似性搜索:快速找到与查询向量最相似的元素
- 推荐系统:基于向量相似度进行内容推荐
- 语义搜索:结合文本嵌入实现语义检索
- AI 应用:支持 RAG(检索增强生成)等场景
Vector Sets 与 Redis Query Engine 的区别
Redis 现在提供两种互补的向量搜索能力:
| 特性 | Vector Sets | Redis Query Engine |
|---|---|---|
| 定位 | 轻量级原生数据类型 | 企业级搜索引擎 |
| 搜索类型 | 向量相似性 | 向量 + 全文 + 数值 + 地理 |
| API 复杂度 | 简单直观 | 功能丰富 |
| 扩展性 | 单机优化 | 支持水平扩展 |
| 适用场景 | 纯向量相似性搜索 | 混合查询、大规模数据 |
何时选择 Vector Sets:
- 应用主要关注向量相似性搜索
- 不需要全文搜索或复杂混合查询
- 追求简单、轻量的解决方案
- AI 驱动的应用需要高效存储和查询嵌入向量
何时选择 Redis Query Engine:
- 需要综合搜索和查询能力
- 需要全文搜索、数值过滤、地理操作的组合
- 大规模数据需要水平扩展
- 企业级应用需要高可用性
核心特性
| 特性 | 说明 |
|---|---|
| 原生支持 | 无需安装额外模块,开箱即用 |
| HNSW 算法 | 使用分层导航小世界算法实现高效检索 |
| 内存高效 | 支持向量量化,减少内存占用 |
| 混合搜索 | 支持向量相似性与属性过滤的结合 |
| 属性存储 | 每个元素可关联 JSON 格式的元数据 |
HNSW 算法简介
HNSW(Hierarchical Navigable Small World)是目前向量搜索领域最主流的近似最近邻(ANN)算法:
层级结构示意:
Level 2: [Head]─────────────────────>[Tail]
│ │
Level 1: [Head]──────>[B]────────────>[Tail]
│ │ │
Level 0: [Head]──>[A]──>[B]──>[C]──>[D]──>[Tail]
核心思想:
- 构建多层图结构,高层稀疏、底层稠密
- 搜索时从高层开始快速定位区域,逐层下沉
- 时间复杂度接近 O(log N),远优于暴力搜索的 O(N)
基本命令
VADD 添加向量
向 Vector Set 添加元素和对应的向量:
# 语法
VADD key [NOCREATE] [REDUCE dim] [QUANTIZATION {q8|bf|no}] [EF build_ef] [SETATTR json_attr] vector element
# 参数说明
# key - Vector Set 的键名
# vector - 向量数据的字符串表示(如 "1.0,2.0,3.0")
# element - 元素名称
# NOCREATE - 如果集合不存在则报错,不自动创建
# REDUCE - 使用随机投影降维到指定维度
# QUANTIZATION - 量化类型:q8(8位,默认)、bf(二进制)、no(不量化)
# EF - 构建时的 ef 参数(影响索引质量,默认200)
# SETATTR - 设置元素的 JSON 属性
# 添加二维向量
127.0.0.1:6379> VADD points "1.0,1.0" point_A
(integer) 1
127.0.0.1:6379> VADD points "-1.0,-1.0" point_B
(integer) 1
127.0.0.1:6379> VADD points "-1.0,1.0" point_C
(integer) 1
127.0.0.1:6379> VADD points "1.0,-1.0" point_D
(integer) 1
# 添加带属性的向量
127.0.0.1:6379> VADD products "0.5,0.3,0.8" prod_001 SETATTR '{"name":"Redis实战","category":"tech","price":89}'
(integer) 1
# 使用高精度(不量化)
127.0.0.1:6379> VADD precise_data "0.1,0.2,0.3" item_001 QUANTIZATION no
(integer) 1
# 使用二进制量化(内存最省)
127.0.0.1:6379> VADD binary_data "0.1,0.2,0.3" item_002 QUANTIZATION bf
(integer) 1
# 使用降维(从高维降到低维)
127.0.0.1:6379> VADD reduced "0.1,0.2,...,0.768" doc_001 REDUCE 128
(integer) 1
向量格式说明:
- 向量以逗号分隔的浮点数字符串表示
- 同一 Vector Set 中所有向量维度必须一致
- 默认使用 8 位量化以节省内存和提高性能
VSIM 相似性搜索
查找与给定向量或元素最相似的结果:
# 语法
VSIM key [vector | ELE element] [WITHSCORES] [COUNT num] [EF search_ef] [FILTER filter_expr] [FILTER-ATTRS name ...] [TRUTH]
# 参数说明
# vector - 查询向量
# ELE element - 使用已有元素的向量作为查询
# WITHSCORES - 返回相似度分数
# COUNT - 返回结果数量(默认10)
# EF - 搜索时的 ef 参数(影响召回率)
# FILTER - 过滤表达式
# FILTER-ATTRS - 指定过滤使用的属性名
# TRUTH - 返回精确值而非量化后的近似值
# 使用查询向量搜索
127.0.0.1:6379> VSIM points "1.0,0.5" WITHSCORES COUNT 3
1) "point_A"
2) "0.9999"
3) "point_D"
4) "0.8944"
5) "point_C"
6) "0.4472"
# 使用已有元素搜索(找相似元素)
127.0.0.1:6379> VSIM points ELE point_A WITHSCORES COUNT 3
1) "point_A"
2) "1.0"
3) "point_D"
4) "0.0"
5) "point_C"
6) "0.0"
# 带过滤条件的搜索
127.0.0.1:6379> VSIM products "0.6,0.4,0.7" WITHSCORES COUNT 10 FILTER ".category == 'tech'"
1) "prod_001"
2) "0.9876"
# 使用高精度搜索
127.0.0.1:6379> VSIM points "1.0,0.5" WITHSCORES COUNT 3 TRUTH
过滤表达式语法:
- 使用类似 JSONPath 的语法访问属性
- 支持
==、!=、>、<、>=、<=等比较操作符 - 支持逻辑组合:
&&(与)、||(或) - 属性通过
.前缀访问
# 复杂过滤示例
VSIM products "0.5,0.5,0.5" FILTER ".price < 100 && .category == 'tech'"
VSIM documents "0.3,0.4,0.5" FILTER ".year > 2020 && .lang == 'en'"
VSIM items "0.1,0.2,0.3" FILTER ".status == 'active' || .featured == true"
相似度分数说明:
- 分数范围是 0 到 1,值越大表示越相似
- 与自己比较时分数为 1.0
- 分数基于余弦相似度计算
VEMB 获取向量
获取元素的向量表示:
# 语法
VEMB key element
# 获取向量
127.0.0.1:6379> VEMB points point_A
1) "1.0"
2) "1.0"
# 注意:返回的可能是量化后的近似值
VCARD 获取元素数量
获取 Vector Set 中的元素数量:
127.0.0.1:6379> VCARD points
(integer) 4
VDIM 获取向量维度
获取 Vector Set 中向量的维度:
127.0.0.1:6379> VDIM points
(integer) 2
VINFO 获取详细信息
获取 Vector Set 的元数据:
127.0.0.1:6379> VINFO points
1) quant_type
2) "q8"
3) dim
4) (integer) 2
5) size
6) (integer) 4
7) max_level
8) (integer) 3
9) entry_point
10) "point_A"
...
属性管理
VSETATTR 设置属性
为元素设置 JSON 格式的属性:
# 语法
VSETATTR key element json
# 设置属性
127.0.0.1:6379> VSETATTR points point_A '{"label":"第一象限","value":100}'
OK
# 清空属性
127.0.0.1:6379> VSETATTR points point_A ""
OK
VGETATTR 获取属性
获取元素的属性:
# 语法
VGETATTR key element
127.0.0.1:6379> VGETATTR points point_A
"{\"label\":\"第一象限\",\"value\":100}"
其他命令
VREM 删除元素
从 Vector Set 中删除元素:
# 语法
VREM key element
127.0.0.1:6379> VREM points point_A
(integer) 1
VRANDMEMBER 随机获取元素
随机获取 Vector Set 中的元素:
# 获取一个随机元素
127.0.0.1:6379> VRANDMEMBER points
"point_C"
# 获取多个随机元素
127.0.0.1:6379> VRANDMEMBER points 2
1) "point_B"
2) "point_D"
VLINKS 获取邻居信息
获取元素在 HNSW 图中的邻居连接信息:
# 语法
VLINKS key element
127.0.0.1:6379> VLINKS points point_A
1) (integer) 0
2) 1) "point_D"
2) "point_C"
实际应用示例
场景一:文档语义搜索
假设我们有一个文档库,每个文档已经被转换为向量(通过嵌入模型如 OpenAI 的 text-embedding-ada-002):
import redis
import numpy as np
# 连接 Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def index_document(doc_id, text, embedding, metadata=None):
"""索引文档"""
# 将向量转换为字符串
vector_str = ",".join(map(str, embedding))
# 添加到 Vector Set
if metadata:
r.execute_command('VADD', 'documents', vector_str, doc_id,
'SETATTR', metadata)
else:
r.execute_command('VADD', 'documents', vector_str, doc_id)
def search_similar(query_embedding, top_k=10, filters=None):
"""搜索相似文档"""
vector_str = ",".join(map(str, query_embedding))
args = ['VSIM', 'documents', vector_str, 'WITHSCORES', 'COUNT', str(top_k)]
if filters:
args.extend(['FILTER', filters])
results = r.execute_command(*args)
# 解析结果
documents = []
for i in range(0, len(results), 2):
documents.append({
'doc_id': results[i],
'score': float(results[i + 1])
})
return documents
# 使用示例
doc_embedding = np.random.rand(1536).tolist() # 假设是 1536 维向量
index_document('doc_001', 'Redis 入门教程', doc_embedding,
'{"title":"Redis入门","category":"tech","views":1000}')
query_embedding = np.random.rand(1536).tolist()
results = search_similar(query_embedding, top_k=5)
场景二:商品推荐系统
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def add_product(product_id, name, category, price, features_vector):
"""添加商品"""
vector_str = ",".join(map(str, features_vector))
attr = json.dumps({
'name': name,
'category': category,
'price': price
})
r.execute_command('VADD', 'products', vector_str, product_id, 'SETATTR', attr)
def recommend_products(user_preference_vector, category=None, max_price=None, top_k=10):
"""推荐商品"""
vector_str = ",".join(map(str, user_preference_vector))
# 构建过滤条件
filters = []
if category:
filters.append(f".category == '{category}'")
if max_price:
filters.append(f".price <= {max_price}")
filter_expr = " && ".join(filters) if filters else None
args = ['VSIM', 'products', vector_str, 'WITHSCORES', 'COUNT', str(top_k)]
if filter_expr:
args.extend(['FILTER', filter_expr])
results = r.execute_command(*args)
recommendations = []
for i in range(0, len(results), 2):
product_id = results[i]
score = float(results[i + 1])
attr = r.execute_command('VGETATTR', 'products', product_id)
recommendations.append({
'product_id': product_id,
'score': score,
'attributes': json.loads(attr) if attr else {}
})
return recommendations
# 使用示例
add_product('prod_001', 'Redis实战', 'tech', 89, [0.5, 0.3, 0.8])
add_product('prod_002', 'Python编程', 'tech', 79, [0.6, 0.4, 0.7])
add_product('prod_003', '算法导论', 'tech', 129, [0.7, 0.5, 0.6])
user_vector = [0.55, 0.35, 0.75]
recommendations = recommend_products(user_vector, category='tech', max_price=100)
场景三:人脸相似度搜索
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def register_face(user_id, face_embedding, user_info):
"""注册人脸"""
vector_str = ",".join(map(str, face_embedding))
attr = json.dumps(user_info)
r.execute_command('VADD', 'faces', vector_str, user_id, 'SETATTR', attr)
def identify_face(face_embedding, threshold=0.95, top_k=1):
"""识别人脸"""
vector_str = ",".join(map(str, face_embedding))
results = r.execute_command('VSIM', 'faces', vector_str,
'WITHSCORES', 'COUNT', str(top_k))
if not results:
return None
user_id = results[0]
similarity = float(results[1])
if similarity >= threshold:
attr = r.execute_command('VGETATTR', 'faces', user_id)
return {
'user_id': user_id,
'similarity': similarity,
'user_info': json.loads(attr) if attr else {}
}
return None
# 使用示例
face_embedding = [0.1, 0.2, 0.3, ...] # 512 维人脸特征向量
register_face('user_001', face_embedding, {'name': '张三', 'dept': '研发部'})
# 识别
query_embedding = [0.1, 0.21, 0.31, ...]
result = identify_face(query_embedding)
if result:
print(f"识别成功: {result['user_info']['name']}")
场景四:RAG 检索增强生成
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def index_knowledge(doc_id, content, embedding, source, page=None):
"""索引知识库"""
vector_str = ",".join(map(str, embedding))
attr = json.dumps({
'content': content,
'source': source,
'page': page
})
r.execute_command('VADD', 'knowledge_base', vector_str, doc_id, 'SETATTR', attr)
def retrieve_context(query_embedding, top_k=5):
"""检索相关上下文"""
vector_str = ",".join(map(str, query_embedding))
results = r.execute_command('VSIM', 'knowledge_base', vector_str,
'WITHSCORES', 'COUNT', str(top_k))
contexts = []
for i in range(0, len(results), 2):
doc_id = results[i]
score = float(results[i + 1])
attr = r.execute_command('VGETATTR', 'knowledge_base', doc_id)
if attr:
data = json.loads(attr)
contexts.append({
'doc_id': doc_id,
'score': score,
'content': data.get('content'),
'source': data.get('source')
})
return contexts
def rag_query(query_embedding, llm_client):
"""RAG 查询"""
# 检索相关上下文
contexts = retrieve_context(query_embedding)
# 构建提示词
context_text = "\n".join([c['content'] for c in contexts])
prompt = f"根据以下上下文回答问题:\n\n{context_text}\n\n问题:..."
# 调用 LLM
response = llm_client.generate(prompt)
return response
性能参数调优
HNSW 参数
Vector Sets 使用 HNSW(分层导航小世界)算法进行高效的近似最近邻搜索。理解以下参数有助于优化性能:
| 参数 | 说明 | 默认值 | 调优建议 |
|---|---|---|---|
| EF(构建时) | 构建索引时的邻居数量 | 200 | 增大可提高召回率,但增加构建时间 |
| EF(搜索时) | 搜索时探索的邻居数量 | 动态 | 增大可提高召回率,但增加延迟 |
# 构建时指定更高的 EF
VADD my_vectors "0.1,0.2,..." element1 EF 400
# 搜索时指定更高的 EF 以获得更好的召回率
VSIM my_vectors "0.1,0.2,..." WITHSCORES COUNT 20 EF 500
构建与搜索权衡
- 高召回率配置:构建 EF = 400,搜索 EF = 500
- 平衡配置:构建 EF = 200(默认),搜索 EF = 100-200
- 高性能配置:构建 EF = 100,搜索 EF = 50
# 高召回率场景(如推荐系统)
VADD products "0.5,0.3,..." prod_001 EF 400
VSIM products "0.5,0.3,..." COUNT 10 EF 500
# 高性能场景(如实时搜索)
VADD realtime "0.5,0.3,..." item_001 EF 100
VSIM realtime "0.5,0.3,..." COUNT 10 EF 50
COUNT 与 EF 的关系
COUNT指定返回结果数量EF影响搜索过程中的探索范围- 通常
EF应该大于或等于COUNT - 更大的
EF/COUNT比值意味着更高的召回率
内存优化
量化选项
Vector Sets 支持多种量化类型以减少内存占用。量化在添加第一个元素时设置,之后无法更改:
| 量化类型 | 参数值 | 说明 | 内存占用 | 适用场景 |
|---|---|---|---|---|
| 8 位量化 | q8(默认) | 将浮点数量化为 8 位整数 | 约 1/4 | 大多数场景 |
| 二进制量化 | bf | 将向量二值化 | 约 1/32 | 内存敏感场景 |
| 不量化 | no | 保持原始精度 | 最高 | 需要高精度场景 |
# 使用默认的 8 位量化
VADD my_vectors "0.1,0.2,..." element1
# 不使用量化(最高精度)
VADD my_vectors "0.1,0.2,..." element1 QUANTIZATION no
# 使用二进制量化(最省内存)
VADD my_vectors "0.1,0.2,..." element1 QUANTIZATION bf
降维功能
Vector Sets 支持通过随机投影进行降维,可以在添加向量时减少维度:
# 将高维向量降维到 128 维
VADD documents "0.1,0.2,...,0.1536" doc_001 REDUCE 128
降维的优势:
- 减少内存占用
- 提高搜索速度
- 适合维度非常高(如 1536+)的场景
注意事项:
- 降维会带来一定的精度损失
- 同一集合中的所有向量必须使用相同的降维设置
内存估算
内存占用 ≈ 向量维度 × 元素数量 × 每维度字节数 + HNSW 图开销
示例:100万向量,1536维,8位量化
≈ 1536 × 1,000,000 × 1 字节 ≈ 1.5 GB
+ HNSW 图开销约 20-30%
≈ 总计约 2 GB
二进制量化时:
≈ 1536 × 1,000,000 / 8 字节 ≈ 192 MB
+ HNSW 图开销
≈ 总计约 250 MB
与其他方案的对比
Vector Sets vs Redis Query Engine
| 特性 | Vector Sets | Redis Query Engine |
|---|---|---|
| 定位 | 原生数据类型 | 企业级搜索引擎 |
| 安装要求 | 内置于 Redis 8.0 | 内置于 Redis 8.0 |
| API 复杂度 | 简单直观 | 功能丰富 |
| 向量搜索 | 支持 | 支持 |
| 全文搜索 | 不支持 | 支持 |
| 混合查询 | 属性过滤 | 完整支持 |
| 水平扩展 | 单机优化 | 支持 |
| 适用规模 | 中小规模 | 大规模 |
选择建议
选择 Vector Sets 当:
- 应用主要关注向量相似性搜索
- 不需要全文搜索或复杂混合查询
- 追求简单、轻量的解决方案
- 数据量适中(百万级向量)
选择 Redis Query Engine 当:
- 需要向量搜索与全文搜索结合
- 需要复杂的混合查询
- 数据量大,需要水平扩展
- 需要索引 Hash、JSON 等 Redis 数据结构
最佳实践
1. 向量归一化
import numpy as np
def normalize_vector(vector):
"""归一化向量"""
norm = np.linalg.norm(vector)
if norm == 0:
return vector
return (vector / norm).tolist()
# 使用归一化后的向量
embedding = normalize_vector(raw_embedding)
vector_str = ",".join(map(str, embedding))
2. 合理设置维度
- 文本嵌入:通常 768-1536 维
- 图像特征:通常 512-2048 维
- 维度越高,精度越好,但内存和计算成本也越高
3. 定期重建索引
# 对于频繁更新的场景,可以定期重建索引以优化性能
def rebuild_index(r, key):
"""重建 Vector Set 索引"""
# 获取所有元素
elements = r.execute_command('VRANDMEMBER', key, -1)
# ... 重新添加逻辑
4. 使用连接池
import redis
from redis.connection import ConnectionPool
pool = ConnectionPool(host='localhost', port=6379, max_connections=100)
r = redis.Redis(connection_pool=pool)
限制与注意事项
- 维度一致性:同一 Vector Set 中所有向量必须维度相同
- 内存限制:大量向量会占用大量内存,需合理规划
- 更新成本:频繁更新会影响 HNSW 图的性能
- 版本要求:需要 Redis 8.0+ 或 Redis Stack 8.0+
小结
本章我们学习了:
- Vector Sets 概述:Redis 8.0 引入的原生向量数据类型,支持高效相似性搜索
- 核心特性:量化存储、降维、属性过滤、HNSW 索引
- 基本命令:VADD、VSIM、VEMB、VCARD、VDIM、VINFO 等
- 属性管理:VSETATTR 设置属性,VSIM 中使用 FILTER 过滤
- 应用场景:语义搜索、推荐系统、人脸识别、RAG 检索增强生成
- 性能调优:EF 参数调整召回率与速度的平衡
- 内存优化:量化选项和降维功能
练习
- 创建一个 Vector Set 并添加多个二维向量,测试相似性搜索
- 使用属性过滤功能筛选特定类别的相似结果
- 对比不同量化类型对搜索结果和内存占用的影响
- 实现一个简单的商品推荐系统原型