Redis 提供了丰富的数据结构,可以很好地适配各种领域应用。RedisVL 作为一个 Python 客户端库,让您能方便地使用 Redis 的搜索和查询 功能,并支持多种存储格式。本文介绍如何使用 RedisVL 操作 Hash 和 JSON 两种存储类型。
前置条件
- 安装了 RedisVL:
pip install redisvl - 有一台运行中的 Redis 实例(Redis 8+ 或 Redis Cloud 均可)
要点
- 理解 Hash 与 JSON 存储类型的本质差异
- 分别为 Hash 和 JSON 定义搜索索引(Schema)
- 使用两种存储类型加载数据和执行查询
- 通过 JSONPath 表达式访问 JSON 中的嵌套字段
- 根据应用需求做出合适的存储类型决策
示例数据集预览
包含用户信息的小型数据集,包含姓名、年龄、职业、信用评分、办公位置、用户向量(3 维)和最后更新时间。先用 pickle 加载数据,并用辅助函数打印:
python
import pickle
from redisvl.redis.utils import buffer_to_array
from redisvl.index import SearchIndex
# 加载示例数据
data = pickle.load(open("hybrid_example_data.pkl", "rb"))
# 辅助打印函数(非标准库,仅用于展示)
from jupyterutils import result_print, table_print
table_print(data)
| user | age | job | credit_score | office_location | user_embedding | last_updated |
|---|---|---|---|---|---|---|
| john | 18 | engineer | high | -122.4194,37.7749 | b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?' | 1741627789 |
| derrick | 14 | doctor | low | -122.4194,37.7749 | b'\xcd\xcc\xcc=\xcd\xcc\xcc=\x00\x00\x00?' | 1741627789 |
| nancy | 94 | doctor | high | -122.4194,37.7749 | b'333?\xcd\xcc\xcc=\x00\x00\x00?' | 1710696589 |
| tyler | 100 | engineer | high | -122.0839,37.3861 | b'\xcd\xcc\xcc=\xcd\xcc\xcc>\x00\x00\x00?' | 1742232589 |
| tim | 12 | dermatologist | high | -122.0839,37.3861 | b'\xcd\xcc\xcc>\xcd\xcc\xcc>\x00\x00\x00?' | 1739644189 |
| taimur | 15 | CEO | low | -122.0839,37.3861 | b'\x9a\x99\x19?\xcd\xcc\xcc=\x00\x00\x00?' | 1742232589 |
| joe | 35 | dentist | medium | -122.0839,37.3861 | b'fff?fff?\xcd\xcc\xcc=' | 1742232589 |
说明 :
user_embedding列中是序列化后的向量字节串(适用于 Hash 存储)。后面处理 JSON 时将其转换为浮点数组。
Hash vs JSON:
Redis 的 Hash 和 JSON 是两种截然不同的数据结构,它们各有特点和适用场景。下面用一个清晰的对比来理解。
Hash(哈希)
Hash 在 Redis 中相当于一个扁平的、单层的键值对集合,就像一张只有一行的表,每个字段对应一个值。例如:
json
{
"model": "Deimos",
"brand": "Ergonom",
"type": "Enduro bikes",
"price": 4972
}
优点:
- 性能卓越:内存占用小,读写速度极快(特别是对于大量简单字段)。
- 存储高效:适合存储规范化、结构固定的数据。
缺点:
- 不支持嵌套:所有字段必须在同一层级,无法表示复杂对象。
- 向量必须序列化:向量数据需以字节串形式存储(稍后详述)。
适用场景:
- 您最关心速度和内存开销。
- 数据可以方便地映射为"字段-值"的字典,无需嵌套。
- 默认推荐:在没有特殊需求时优先考虑 Hash。
JSON(JavaScript Object Notation)
JSON 是 Redis 原生支持的一种文档格式,可以包含多层嵌套对象和数组。例如:
json
{
"name": "Specialized Stump jumper",
"metadata": {
"model": "Stumpjumper",
"brand": "Specialized",
"type": "Enduro bikes",
"price": 3000
}
}
优点:
- 灵活建模:可以表示任意复杂的嵌套结构,贴近应用层的对象模型。
- 原生支持:许多应用已经使用 JSON 作为数据交换格式,迁移成本低。
- JSONPath 支持:可以精确更新或查询嵌套子元素。
缺点:
- 性能开销:相比 Hash 稍大(内存和 CPU 消耗),但现代 Redis 对此有很好优化。
- 向量存储为数组:向量需以浮点数列表形式存储(稍后详述)。
适用场景:
- 数据本身已经是 JSON 格式,或者需要表达嵌套关系。
- 您希望用一套方案替代其他文档数据库(如 MongoDB)。
- 需要灵活检索嵌套字段。
决策流程图
下面用一张图帮您快速判断该选哪种存储类型:
#mermaid-svg-PCRD0BAqz47Dwl42{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PCRD0BAqz47Dwl42 .error-icon{fill:#552222;}#mermaid-svg-PCRD0BAqz47Dwl42 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PCRD0BAqz47Dwl42 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PCRD0BAqz47Dwl42 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PCRD0BAqz47Dwl42 .marker.cross{stroke:#333333;}#mermaid-svg-PCRD0BAqz47Dwl42 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PCRD0BAqz47Dwl42 p{margin:0;}#mermaid-svg-PCRD0BAqz47Dwl42 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster-label text{fill:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster-label span{color:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster-label span p{background-color:transparent;}#mermaid-svg-PCRD0BAqz47Dwl42 .label text,#mermaid-svg-PCRD0BAqz47Dwl42 span{fill:#333;color:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 .node rect,#mermaid-svg-PCRD0BAqz47Dwl42 .node circle,#mermaid-svg-PCRD0BAqz47Dwl42 .node ellipse,#mermaid-svg-PCRD0BAqz47Dwl42 .node polygon,#mermaid-svg-PCRD0BAqz47Dwl42 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PCRD0BAqz47Dwl42 .rough-node .label text,#mermaid-svg-PCRD0BAqz47Dwl42 .node .label text,#mermaid-svg-PCRD0BAqz47Dwl42 .image-shape .label,#mermaid-svg-PCRD0BAqz47Dwl42 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PCRD0BAqz47Dwl42 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PCRD0BAqz47Dwl42 .rough-node .label,#mermaid-svg-PCRD0BAqz47Dwl42 .node .label,#mermaid-svg-PCRD0BAqz47Dwl42 .image-shape .label,#mermaid-svg-PCRD0BAqz47Dwl42 .icon-shape .label{text-align:center;}#mermaid-svg-PCRD0BAqz47Dwl42 .node.clickable{cursor:pointer;}#mermaid-svg-PCRD0BAqz47Dwl42 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PCRD0BAqz47Dwl42 .arrowheadPath{fill:#333333;}#mermaid-svg-PCRD0BAqz47Dwl42 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PCRD0BAqz47Dwl42 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PCRD0BAqz47Dwl42 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PCRD0BAqz47Dwl42 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PCRD0BAqz47Dwl42 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PCRD0BAqz47Dwl42 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster text{fill:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 .cluster span{color:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PCRD0BAqz47Dwl42 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PCRD0BAqz47Dwl42 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PCRD0BAqz47Dwl42 .icon-shape,#mermaid-svg-PCRD0BAqz47Dwl42 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PCRD0BAqz47Dwl42 .icon-shape p,#mermaid-svg-PCRD0BAqz47Dwl42 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PCRD0BAqz47Dwl42 .icon-shape .label rect,#mermaid-svg-PCRD0BAqz47Dwl42 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PCRD0BAqz47Dwl42 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PCRD0BAqz47Dwl42 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PCRD0BAqz47Dwl42 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
开始选择存储类型
数据是否有嵌套结构?
JSON
是否追求极致性能和内存?
Hash
数据是否已是 JSON 格式?
使用 JSON 存储
使用 Hash 存储
使用 Hash 存储
1. 定义 Hash 索引结构
在 RedisVL 中,索引模式(Schema)通过字典定义。对于 Hash,我们在 index 部分指定 "storage_type": "hash"(该值为默认值,可以省略)。字段定义与普通搜索索引一致,包含标签、文本、数值、地理位置和向量等类型。
python
hash_schema = {
"index": {
"name": "user-hash", # 索引名称
"prefix": "user-hash-docs", # 存储键的前缀
"storage_type": "hash", # 指定为 Hash 存储(默认)
},
"fields": [
{"name": "user", "type": "tag"},
{"name": "credit_score", "type": "tag"},
{"name": "job", "type": "text"},
{"name": "age", "type": "numeric"},
{"name": "office_location", "type": "geo"},
{
"name": "user_embedding",
"type": "vector",
"attrs": {
"dims": 3,
"distance_metric": "cosine",
"algorithm": "flat",
"datatype": "float32"
}
}
],
}
2. 创建索引并加载数据
利用 SearchIndex.from_dict 构造索引对象,然后调用 create 方法在 Redis 中创建索引。最后使用 load 方法批量加载数据。
python
# 构建索引对象
hindex = SearchIndex.from_dict(hash_schema, redis_url="redis://localhost:6379")
# 创建索引(如果已存在则覆盖)
hindex.create(overwrite=True)
# 查看存储类型
print(hindex.storage_type) # <StorageType.HASH: 'hash'>
重要:向量字段处理(Hash 专用)
在 Hash 存储中,向量数据必须以**字节串(bytes)**形式存储,这是为了在 Redis 内部高效索引和计算。示例数据中的 user_embedding 已经是字节串,因此可以直接加载:
python
# 加载数据(每条记录自动以 prefix + 唯一 ID 存入 Hash)
keys = hindex.load(data)
print(keys) # 返回存储的键列表
3. 检查索引统计信息
您可以使用 RedisVL 命令行工具查看索引状态(例如文档数、向量索引大小等):
bash
rvl stats -i user-hash
输出类似:
Statistics:
╭─────────────────────────────┬────────────╮
│ Stat Key │ Value │
├─────────────────────────────┼────────────┤
│ num_docs │ 7 │
│ num_terms │ 6 │
│ ... │ ... │
│ vector_index_sz_mb │ 0.02820587 │
╰─────────────────────────────┴────────────╯
4. 执行查询
RedisVL 的查询构造器支持组合条件(标签、文本、数值)和向量相似性搜索。
组合过滤器:例如,查询信用分高、职业包含 "engineer"(支持通配符)、年龄大于 17 岁的用户:
python
from redisvl.query import VectorQuery
from redisvl.query.filter import Tag, Text, Num
# 构建过滤条件
filter_expr = (Tag("credit_score") == "high") & (Text("job") % "enginee*") & (Num("age") > 17)
# 构造向量查询
query = VectorQuery(
vector=[0.1, 0.1, 0.5], # 查询向量
vector_field_name="user_embedding", # 向量字段名
return_fields=["user", "credit_score", "age", "job", "office_location"],
filter_expression=filter_expr
)
# 执行查询
results = hindex.query(query)
result_print(results)
结果会按照向量相似度(余弦距离)升序排列,并返回指定的字段:
| vector_distance | user | credit_score | age | job | office_location |
|---|---|---|---|---|---|
| 0 | john | high | 18 | engineer | -122.4194,37.7749 |
| 0.109129190445 | tyler | high | 100 | engineer | -122.0839,37.3861 |
距离为 0 表示查询向量与 john 的向量完全相同(因为示例数据中 john 的向量正好是 0.1, 0.1, 0.5)。
5. 清理索引
python
hindex.delete()
使用 JSON 存储
1. 定义 JSON 索引结构
JSON 的索引定义与 Hash 几乎相同,唯一区别在于 "storage_type": "json"。
python
json_schema = {
"index": {
"name": "user-json",
"prefix": "user-json-docs",
"storage_type": "json", # 显式指定 JSON
},
"fields": [ # 字段定义与 Hash 一致
{"name": "user", "type": "tag"},
{"name": "credit_score", "type": "tag"},
{"name": "job", "type": "text"},
{"name": "age", "type": "numeric"},
{"name": "office_location", "type": "geo"},
{
"name": "user_embedding",
"type": "vector",
"attrs": {
"dims": 3,
"distance_metric": "cosine",
"algorithm": "flat",
"datatype": "float32"
}
}
],
}
2. 创建索引
python
jindex = SearchIndex.from_dict(json_schema, redis_url="redis://localhost:6379")
jindex.create(overwrite=True)
3. 数据格式转换(向量字段)
对于 JSON 存储,向量字段必须是浮点数列表(Python list) ,而不是字节串。我们需要将数据中的 user_embedding 由字节串转换为数组:
python
json_data = data.copy()
for d in json_data:
d['user_embedding'] = buffer_to_array(d['user_embedding'], dtype='float32')
转换后的记录示例:
python
{
'user': 'john',
'age': 18,
'job': 'engineer',
'credit_score': 'high',
'office_location': '-122.4194,37.7749',
'user_embedding': [0.1, 0.1, 0.5], # 变为列表
'last_updated': 1741627789
}
然后加载数据:
python
keys = jindex.load(json_data)
4. 执行同样的查询
由于字段名和类型一致,我们可以复用之前定义的 VectorQuery 对象:
python
result_print(jindex.query(query))
结果与 Hash 版本完全相同。
5. 清理
python
jindex.delete()
JSON 嵌套数据与 JSONPath 支持
JSON 的"王牌"功能是支持嵌套对象。当您需要索引深层字段时,必须在模式中指定路径(path) ,格式为 $.object.attribute。如果未指定 path,RedisVL 默认使用 $.{name}(即根级字段)。
下面我们用一个自行车商品示例来演示如何索引嵌套元数据并基于向量检索。
1. 生成向量嵌入
我们使用 HuggingFace 文本向量化工具(HFTextVectorizer)将描述文字转换为向量:
python
from redisvl.utils.vectorize import HFTextVectorizer
emb_model = HFTextVectorizer()
bike_data = [
{
"name": "Specialized Stump jumper",
"metadata": {
"model": "Stumpjumper",
"brand": "Specialized",
"type": "Enduro bikes",
"price": 3000
},
"description": "The Specialized Stumpjumper is a versatile enduro bike that dominates both climbs and descents. Features a FACT 11m carbon fiber frame, FOX FLOAT suspension with 160mm travel, and SRAM X01 Eagle drivetrain. The asymmetric frame design and internal storage compartment make it a practical choice for all-day adventures."
},
{
"name": "bike_2",
"metadata": {
"model": "Slash",
"brand": "Trek",
"type": "Enduro bikes",
"price": 5000
},
"description": "Trek's Slash is built for aggressive enduro riding and racing. Featuring Trek's Alpha Aluminum frame with RE:aktiv suspension technology, 160mm travel, and Knock Block frame protection. Equipped with Bontrager components and a Shimano XT drivetrain, this bike excels on technical trails and enduro race courses."
}
]
# 为每条数据生成向量嵌入,并添加到原字典中
bike_data = [{**d, "bike_embedding": emb_model.embed(d["description"])} for d in bike_data]
2. 定义索引模式(含 JSONPath)
在字段定义中,使用 path 指向嵌套字段:
python
bike_schema = {
"index": {
"name": "bike-json",
"prefix": "bike-json",
"storage_type": "json",
},
"fields": [
{
"name": "model",
"type": "tag",
"path": "$.metadata.model" # 指向 metadata.model
},
{
"name": "brand",
"type": "tag",
"path": "$.metadata.brand"
},
{
"name": "price",
"type": "numeric",
"path": "$.metadata.price"
},
{
"name": "bike_embedding",
"type": "vector",
"attrs": {
"dims": len(bike_data[0]["bike_embedding"]),
"distance_metric": "cosine",
"algorithm": "flat",
"datatype": "float32"
}
}
],
}
3. 创建索引并加载数据
python
bike_index = SearchIndex.from_dict(bike_schema, redis_url="redis://localhost:6379")
bike_index.create(overwrite=True)
bike_index.load(bike_data)
4. 查询并返回嵌套字段
我们使用自然语言查询:"I'd like a bike for aggressive riding"(我想要一辆适合激进骑行的自行车),将其向量化后执行检索。
在 return_fields 中,如果想返回未索引的嵌套字段(例如 type),必须提供完整的 JSONPath($.metadata.type):
python
query_vector = emb_model.embed("I'd like a bike for aggressive riding")
query = VectorQuery(
vector=query_vector,
vector_field_name="bike_embedding",
return_fields=[
"brand", # 索引字段,直接返回
"name", # 根级字段
"$.metadata.type" # 非索引字段,需用完整路径
]
)
results = bike_index.query(query)
print(results)
输出:
python
[
{'id': 'bike-json:01KHKJ5WW3DJE0X6E85GG27V0Y',
'vector_distance': '0.519988954067',
'brand': 'Trek',
'$.metadata.type': 'Enduro bikes'},
{'id': 'bike-json:01KHKJ5WW3DJE0X6E85GG27V0X',
'vector_distance': '0.65762424469',
'brand': 'Specialized',
'$.metadata.type': 'Enduro bikes'}
]
注意 :返回字段中的
$.metadata.type虽然未在索引中定义,但只要提供路径,Redis 仍能正确提取该值。若您希望过滤或排序该字段,则必须将其加入索引字段列表并指定路径。
总结与推荐
| 特性 | Hash | JSON |
|---|---|---|
| 数据结构 | 扁平键值对 | 嵌套文档(对象/数组) |
| 向量存储格式 | 字节串(bytes) | 浮点数列表 |
| 嵌套字段访问 | 不支持 | 支持 JSONPath |
| 性能 | 极高,内存占用小 | 良好,但比 Hash 稍高 |
| 灵活性 | 低 | 高 |
| 适用场景 | 标准化、固定结构、高性能需求 | 复杂对象、现成 JSON 数据、需灵活检索 |
最终建议:
- 如果不确定,优先选择 Hash,它简单高效,足以应付大多数场景。
- 当数据天然具有层级关系,或者您希望保留完整的 JSON 文档结构时,请选择 JSON。
- 如果您的应用已经使用其他文档数据库,迁移到 Redis JSON 可以保持相似的编程模型。