Skip to content

Commit 31ded2e

Browse files
authored
Add RedisVL integration for advanced vector search (#791)
* Add RedisVL integration for advanced vector search - Add redisvl as optional dependency (pip install redis-om[redisvl]) - Add to_redisvl_schema() to convert OM models to RedisVL IndexSchema - Add get_redisvl_index() to get ready-to-use SearchIndex - Supports all OM field types: text, tag, numeric, vector - Supports FLAT and HNSW vector algorithms with all parameters Refs #790 * Make redisvl a required dependency * Add vector fields and RedisVL integration docs * Add vector search terms to spellcheck wordlist * Add DSL to spellcheck wordlist * Add tests for RedisVL integration * Fix AsyncSearchIndex to SearchIndex mapping for unasync
1 parent abfcbfa commit 31ded2e

7 files changed

Lines changed: 576 additions & 1 deletion

File tree

.github/wordlist.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,13 @@ ulid
111111
coroutine
112112
compat
113113
programmatically
114-
uv
114+
uv
115+
RedisVL
116+
embeddings
117+
VectorQuery
118+
VectorRangeQuery
119+
SearchIndex
120+
ADHOC
121+
EF
122+
DSL
123+
DSL

aredis_om/redisvl.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"""
2+
RedisVL integration for Redis OM.
3+
4+
This module provides utilities to convert Redis OM models to RedisVL schemas,
5+
enabling advanced vector search capabilities through RedisVL.
6+
7+
Example:
8+
from redis_om import JsonModel, Field, VectorFieldOptions
9+
from aredis_om.redisvl import to_redisvl_schema, get_redisvl_index
10+
11+
class Document(JsonModel, index=True):
12+
title: str = Field(index=True)
13+
embedding: list[float] = Field(
14+
vector_options=VectorFieldOptions.flat(
15+
type=VectorFieldOptions.TYPE.FLOAT32,
16+
dimension=384,
17+
distance_metric=VectorFieldOptions.DISTANCE_METRIC.COSINE,
18+
)
19+
)
20+
21+
# Get a RedisVL IndexSchema for advanced operations
22+
schema = to_redisvl_schema(Document)
23+
24+
# Or get a ready-to-use SearchIndex
25+
index = get_redisvl_index(Document)
26+
results = await index.query(VectorQuery(...))
27+
"""
28+
29+
from typing import Any, Dict, List, Optional, Type, Union
30+
31+
from redisvl.index import AsyncSearchIndex, SearchIndex
32+
from redisvl.schema import IndexSchema
33+
34+
from .model.model import (
35+
FieldInfo,
36+
JsonModel,
37+
RedisModel,
38+
VectorFieldOptions,
39+
get_outer_type,
40+
is_numeric_type,
41+
is_supported_container_type,
42+
should_index_field,
43+
)
44+
45+
46+
def _get_field_type(
47+
field_name: str,
48+
field_type: Any,
49+
field_info: FieldInfo,
50+
is_json: bool,
51+
) -> Optional[Dict[str, Any]]:
52+
"""Convert an OM field to a RedisVL field definition."""
53+
if not should_index_field(field_info):
54+
return None
55+
56+
vector_options: Optional[VectorFieldOptions] = getattr(
57+
field_info, "vector_options", None
58+
)
59+
sortable = getattr(field_info, "sortable", False) is True
60+
full_text_search = getattr(field_info, "full_text_search", False) is True
61+
case_sensitive = getattr(field_info, "case_sensitive", False) is True
62+
63+
# Vector field
64+
if vector_options:
65+
attrs = {
66+
"dims": vector_options.dimension,
67+
"distance_metric": vector_options.distance_metric.name.lower(),
68+
"algorithm": vector_options.algorithm.name.lower(),
69+
"datatype": vector_options.type.name.lower(),
70+
}
71+
if vector_options.initial_cap:
72+
attrs["initial_cap"] = vector_options.initial_cap
73+
is_flat = vector_options.algorithm.name == "FLAT"
74+
if is_flat and vector_options.block_size:
75+
attrs["block_size"] = vector_options.block_size
76+
if vector_options.algorithm.name == "HNSW":
77+
if vector_options.m:
78+
attrs["m"] = vector_options.m
79+
if vector_options.ef_construction:
80+
attrs["ef_construction"] = vector_options.ef_construction
81+
if vector_options.ef_runtime:
82+
attrs["ef_runtime"] = vector_options.ef_runtime
83+
if vector_options.epsilon:
84+
attrs["epsilon"] = vector_options.epsilon
85+
return {"name": field_name, "type": "vector", "attrs": attrs}
86+
87+
# Numeric field
88+
if is_numeric_type(field_type):
89+
attrs = {"sortable": sortable}
90+
return {"name": field_name, "type": "numeric", "attrs": attrs}
91+
92+
# Boolean - stored as TAG
93+
if field_type is bool:
94+
return {"name": field_name, "type": "tag"}
95+
96+
# String field
97+
if isinstance(field_type, type) and issubclass(field_type, str):
98+
if full_text_search:
99+
attrs = {"sortable": sortable}
100+
return {"name": field_name, "type": "text", "attrs": attrs}
101+
else:
102+
attrs = {"sortable": sortable, "case_sensitive": case_sensitive}
103+
return {"name": field_name, "type": "tag", "attrs": attrs}
104+
105+
# List of strings -> TAG
106+
if is_supported_container_type(field_type):
107+
from typing import get_args
108+
109+
inner_types = get_args(field_type)
110+
if inner_types and inner_types[0] is str:
111+
attrs = {"sortable": sortable}
112+
return {"name": field_name, "type": "tag", "attrs": attrs}
113+
114+
# Default to tag for unknown types
115+
return {"name": field_name, "type": "tag"}
116+
117+
118+
def to_redisvl_schema(model_cls: Type[RedisModel]) -> "IndexSchema":
119+
"""
120+
Convert a Redis OM model to a RedisVL IndexSchema.
121+
122+
This allows you to use RedisVL's advanced query capabilities with your
123+
Redis OM models, including:
124+
- VectorQuery with hybrid policies (BATCHES, ADHOC_BF)
125+
- VectorRangeQuery for epsilon-based searches
126+
- Advanced filter expressions
127+
- EF_RUNTIME tuning for HNSW indexes
128+
129+
Args:
130+
model_cls: A HashModel or JsonModel class with index=True
131+
132+
Returns:
133+
A RedisVL IndexSchema that can be used with SearchIndex
134+
135+
Raises:
136+
ValueError: If the model is not indexed
137+
138+
Example:
139+
schema = to_redisvl_schema(MyModel)
140+
index = SearchIndex(schema=schema, redis_client=redis)
141+
results = await index.query(VectorQuery(...))
142+
"""
143+
# Check if model is indexed
144+
# model_config is a dict in Pydantic v2
145+
model_config = getattr(model_cls, "model_config", {})
146+
if isinstance(model_config, dict):
147+
is_indexed = model_config.get("index", False)
148+
else:
149+
is_indexed = False
150+
if not is_indexed:
151+
raise ValueError(
152+
f"Model {model_cls.__name__} is not indexed. "
153+
"Use 'class MyModel(JsonModel, index=True):' to enable indexing."
154+
)
155+
156+
# Determine storage type
157+
is_json = issubclass(model_cls, JsonModel)
158+
storage_type = "json" if is_json else "hash"
159+
160+
# Get index name and prefix
161+
index_name = model_cls.Meta.index_name
162+
key_prefix = model_cls.make_key("")
163+
164+
# Build field definitions
165+
fields: List[Dict[str, Any]] = []
166+
167+
for name, field in model_cls.model_fields.items():
168+
field_type = get_outer_type(field)
169+
if field_type is None:
170+
continue
171+
172+
# Get FieldInfo (may be wrapped in metadata)
173+
if (
174+
not isinstance(field, FieldInfo)
175+
and hasattr(field, "metadata")
176+
and len(field.metadata) > 0
177+
and isinstance(field.metadata[0], FieldInfo)
178+
):
179+
field_info = field.metadata[0]
180+
elif isinstance(field, FieldInfo):
181+
field_info = field
182+
else:
183+
continue
184+
185+
field_def = _get_field_type(name, field_type, field_info, is_json)
186+
if field_def:
187+
fields.append(field_def)
188+
189+
# Build schema dict
190+
schema_dict = {
191+
"index": {
192+
"name": index_name,
193+
"prefix": key_prefix,
194+
"storage_type": storage_type,
195+
},
196+
"fields": fields,
197+
}
198+
199+
return IndexSchema.from_dict(schema_dict)
200+
201+
202+
def get_redisvl_index(
203+
model_cls: Type[RedisModel],
204+
async_client: bool = True,
205+
) -> Union["AsyncSearchIndex", "SearchIndex"]:
206+
"""
207+
Get a RedisVL SearchIndex for a Redis OM model.
208+
209+
This provides a ready-to-use SearchIndex connected to the model's
210+
Redis database, enabling advanced vector search operations.
211+
212+
Args:
213+
model_cls: A HashModel or JsonModel class with index=True
214+
async_client: If True (default), return AsyncSearchIndex.
215+
If False, return sync SearchIndex.
216+
217+
Returns:
218+
A RedisVL SearchIndex (async or sync) connected to Redis
219+
220+
Raises:
221+
ValueError: If the model is not indexed
222+
223+
Example:
224+
index = get_redisvl_index(MyModel)
225+
results = await index.query(VectorQuery(
226+
vector=query_embedding,
227+
vector_field_name="embedding",
228+
num_results=10,
229+
))
230+
"""
231+
schema = to_redisvl_schema(model_cls)
232+
redis_client = model_cls.db()
233+
234+
if async_client:
235+
return AsyncSearchIndex(schema=schema, redis_client=redis_client)
236+
else:
237+
return SearchIndex(schema=schema, redis_client=redis_client)

0 commit comments

Comments
 (0)