Models and Fields¶
The heart of Redis OM's object mapping, validation, and persistence features is a
pair of declarative models: HashModel and JsonModel. Both models provide
roughly the same API, but they store data in Redis differently.
This page explains how to define Redis OM models. For querying models, see Making Queries.
HashModel vs. JsonModel¶
First, which should you use?
The choice is relatively simple. If you want to embed a model inside another
model, like giving a Customer model a list of Order models, then you need to
use JsonModel. Only JsonModel supports embedded models.
Otherwise, use HashModel.
Creating Your Model¶
You create a Redis OM model by subclassing HashModel or JsonModel. For
example:
Fields¶
You define fields on a Redis OM model using Python type annotations. If you aren't familiar with type annotations, check out this tutorial.
This works exactly the same way as it does with Pydantic. Check out the Pydantic documentation on field types for guidance.
With HashModel¶
HashModel stores data in Redis Hashes, which are flat. This means that a Redis Hash can't contain a Redis Set, List, or Hash. Because of this requirement, HashModel also does not currently support container types, such as:
- Sets
- Lists
- Dictionaries and other "mapping" types
- Other Redis OM models
- Pydantic models
NOTE: In the future, we may serialize these values as JSON strings, the same way we do for JsonModel. The difference would be that in the case of HashModel, you wouldn't be able to index these fields, just get and save them with the model. With JsonModel, you can index list fields and embedded JsonModels.
So, in short, if you want to use container types, use JsonModel.
With JsonModel¶
Good news! Container types are supported with JsonModel.
We will use Pydantic's JSON serialization and encoding to serialize your JsonModel and save it in Redis.
Default Values¶
Fields can have default values. You set them by assigning a value to a field.
import datetime
from typing import Optional
from redis_om import HashModel
class Customer(HashModel):
first_name: str
last_name: str
email: str
join_date: datetime.date
age: int
bio: Optional[str] = "Super dope" # <- We added a default here
Now, if we create a Customer object without a bio field, it will use the default value.
andrew = Customer(
first_name="Andrew",
last_name="Brookins",
email="andrew.brookins@example.com",
join_date=datetime.date.today(),
age=38) # <- Notice, we didn't give a bio!
print(andrew.bio) # <- So we got the default value.
# > 'Super Dope'
The model will then save this default value to Redis the next time you call save().
Optional Fields¶
Fields without default values are required. To make a field optional, use Optional:
from typing import Optional
from redis_om import HashModel
class Customer(HashModel):
first_name: str
last_name: str
bio: Optional[str] = None # Optional with None default
Validation¶
Redis OM uses Pydantic behind the scenes to validate data at runtime based on the model's type annotations.
Every Redis OM model is also a Pydantic model, so you can use Pydantic validators like EmailStr, Pattern, and many more for complex validation.
Basic Type Validation¶
Validation works for basic type annotations like str:
import datetime
from typing import Optional
from pydantic import EmailStr
from redis_om import HashModel
class Customer(HashModel):
first_name: str
last_name: str
email: EmailStr
join_date: datetime.date
age: int
bio: Optional[str]
Redis OM will ensure that first_name is always a string, age is always an integer, and so on.
Complex Validation¶
Let's see what happens if we try to create a Customer object with an invalid email address:
from pydantic import ValidationError
try:
Customer(
first_name="Andrew",
last_name="Brookins",
email="Not an email address!",
join_date=datetime.date.today(),
age=38,
bio="Python developer, works at Redis, Inc."
)
except ValidationError as e:
print(e)
"""
1 validation error for Customer
email
value is not a valid email address: An email address must have an @-sign.
"""
You'll also get a validation error if you change a field on a model instance to an invalid value and then try to save:
andrew = Customer(
first_name="Andrew",
last_name="Brookins",
email="andrew.brookins@example.com",
join_date=datetime.date.today(),
age=38,
bio="Python developer"
)
andrew.email = "Not valid"
try:
andrew.save()
except ValidationError as e:
print(e) # ValidationError: email is not a valid email address
Constrained Values¶
Pydantic includes many type annotations to introduce constraints to your model field values:
- Strings that are always lowercase
- Strings that must match a regular expression
- Integers within a range
- Integers that are a specific multiple
- And many more...
All of these constraint types work with Redis OM models. Read the Pydantic documentation on constrained types to learn more.
Saving and Loading Models¶
Saving Models¶
Save a model to Redis by calling save():
andrew = Customer(
first_name="Andrew",
last_name="Brookins",
email="andrew.brookins@example.com",
join_date=datetime.date.today(),
age=38)
await andrew.save() # Async
# andrew.save() # Sync
Conditional Saves¶
Use nx (only if not exists) or xx (only if exists) for conditional saves:
# Only save if the key does NOT exist (insert-only)
result = await andrew.save(nx=True)
# Only save if the key already exists (update-only)
result = await andrew.save(xx=True)
Returns None if the condition was not met, otherwise returns the model.
Getting a Model by Primary Key¶
If you have the primary key of a model, you can call the get() method:
Automatic Primary Keys¶
Models generate a globally unique primary key automatically without needing to talk to Redis:
andrew = Customer(
first_name="Andrew",
last_name="Brookins",
email="andrew.brookins@example.com",
join_date=datetime.date.today(),
age=38)
print(andrew.pk)
# > '01FJM6PH661HCNNRC884H6K30C'
The ID is available before you save the model. The default ID generation function creates ULIDs.
Updating Models¶
Update a model instance with specific field values:
Deleting Models¶
Delete a model by primary key:
Or delete multiple models:
Expiring Models¶
Set a TTL (time to live) on a model instance:
Listing All Primary Keys¶
Get all primary keys for a model:
Configuring Models¶
There are several Redis OM-specific settings you can configure in models. You configure these settings using a special object called the Meta object.
Here is an example of using the Meta object to set a global key prefix:
from redis_om import HashModel
class Customer(HashModel):
first_name: str
last_name: str
class Meta:
global_key_prefix = "customer-dashboard"
All Settings Supported by the Meta Object¶
| Setting | Description | Default |
|---|---|---|
| global_key_prefix | A string prefix applied to every Redis key that the model manages. This could be something like your application's name. | "" |
| model_key_prefix | A string prefix applied to the Redis key representing every model. For example, the Redis Hash key for a HashModel. This prefix is also added to the redisearch index created for every model with indexed fields. | f"{new_class.module}.{new_class.name}" |
| primary_key_pattern | A format string producing the base string for a Redis key representing this model. This string should accept a "pk" format argument. Note: This is a "new style" format string, which will be called with .format(). |
"{pk}" |
| database | A redis.asyncio.Redis or redis.Redis client instance that the model will use to communicate with Redis. | A new instance created with connections.get_redis_connection(). |
| primary_key_creator_cls | A class that adheres to the PrimaryKeyCreator protocol, which Redis OM will use to create a primary key for a new model instance. | UlidPrimaryKey |
| index_name | The RediSearch index name to use for this model. Only used if the model is indexed (index=True on the model class). |
"{global_key_prefix}:{model_key_prefix}:index" |
| embedded | Whether or not this model is "embedded." Embedded models are not included in migrations that create and destroy indexes. Instead, their indexed fields are included in the index for the parent model. Note: Only JsonModel can have embedded models. |
False |
| encoding | The default encoding to use for strings. This encoding is given to redis-py at the connection level. In both cases, Redis OM will decode binary strings from Redis using your chosen encoding. | "utf-8" |
Abstract Models¶
You can create abstract Redis OM models by subclassing ABC in addition to
either HashModel or JsonModel. Abstract models exist only to gather shared
configuration for subclasses -- you can't instantiate them.
One use of abstract models is to configure a Redis key prefix that all models in your application will use:
from abc import ABC
from redis_om import HashModel
class BaseModel(HashModel, ABC):
class Meta:
global_key_prefix = "your-application"
Meta Object Inheritance¶
The Meta object has a special property: if you create a model subclass from a base class that has a Meta object, Redis OM copies the parent's fields into the Meta object in the child class.
A subclass can override a single field in its parent's Meta class without having to redefine all fields:
from abc import ABC
from redis_om import HashModel, get_redis_connection
redis = get_redis_connection(port=6380)
other_redis = get_redis_connection(port=6381)
class BaseModel(HashModel, ABC):
class Meta:
global_key_prefix = "customer-dashboard"
database = redis
class Customer(BaseModel):
first_name: str
last_name: str
class Meta:
database = other_redis
print(Customer.global_key_prefix)
# > "customer-dashboard" # Inherited from BaseModel
Custom Primary Key Creators¶
By default, Redis OM uses ULID (Universally Unique Lexicographically Sortable Identifier) for primary keys. You can customize this:
import uuid
from redis_om import HashModel
class UUIDv7PrimaryKey:
@staticmethod
def create_pk(*args, **kwargs) -> str:
return str(uuid.uuid7())
class MyModel(HashModel):
name: str
class Meta:
primary_key_creator_cls = UUIDv7PrimaryKey
Note: uuid.uuid7() requires Python 3.11+ or a backport library like uuid6.
Configuring Pydantic¶
Every Redis OM model is also a Pydantic model, so you can control Pydantic configuration via model_config:
from pydantic import ConfigDict
from redis_om import HashModel
class Customer(HashModel):
# ... Fields ...
model_config = ConfigDict(
from_attributes=True,
arbitrary_types_allowed=True,
extra="allow",
)
See the Pydantic documentation for available settings.
Model-Level Indexing¶
If you're using Redis with the Search capability, you can make your model indexed by adding index=True to the model class declaration:
from redis_om import HashModel
class Customer(HashModel, index=True):
first_name: str
last_name: str
email: str
age: int
In this example, all fields in the Customer model will be indexed automatically, enabling queries with find().
Excluding Fields from Indexing¶
You can exclude specific fields from indexing using Field(index=False):
from redis_om import HashModel, Field
class Customer(HashModel, index=True):
first_name: str = Field(index=False) # Not indexed
last_name: str # Indexed (default)
email: str # Indexed (default)
age: int # Indexed (default)
Field-Specific Index Options¶
Control indexing behavior with field-specific options:
from redis_om import HashModel, Field
class Customer(HashModel, index=True):
first_name: str = Field(index=False) # Excluded from index
last_name: str # Indexed as TAG (default)
bio: str = Field(full_text_search=True) # Indexed as TEXT for full-text search
age: int = Field(sortable=True) # Indexed as NUMERIC, sortable
category: str = Field(case_sensitive=False) # Indexed as TAG, case-insensitive
Field Index Types¶
Redis OM automatically chooses the appropriate RediSearch field type based on the Python field type:
| Python Type | RediSearch Field Type | Notes |
|---|---|---|
str |
TAG | Exact matching (default) |
str with full_text_search=True |
TEXT | Full-text search |
int, float |
NUMERIC | Range queries and sorting |
bool |
TAG | Boolean fields |
datetime |
NUMERIC | Stored as Unix timestamps |
| Geographic types | GEO | Location queries |
All field types support sorting when marked with sortable=True.
Running Migrations¶
To create the indexes for indexed models, use the om migrate CLI command:
Or run the Migrator programmatically:
For detailed migration instructions, see Migrations.
Vector Fields¶
Redis OM supports vector fields for similarity search, enabling AI and machine learning applications.
Defining Vector Fields¶
Use VectorFieldOptions to configure vector fields:
from redis_om import JsonModel, Field, VectorFieldOptions
class Document(JsonModel, index=True):
title: str = Field(index=True)
content: str = Field(full_text_search=True)
embedding: list[float] = Field(
vector_options=VectorFieldOptions.flat(
type=VectorFieldOptions.TYPE.FLOAT32,
dimension=384, # Must match your embedding model's output
distance_metric=VectorFieldOptions.DISTANCE_METRIC.COSINE,
)
)
Vector Algorithm Options¶
FLAT - Brute-force search, best for smaller datasets:
vector_options = VectorFieldOptions.flat(
type=VectorFieldOptions.TYPE.FLOAT32,
dimension=768,
distance_metric=VectorFieldOptions.DISTANCE_METRIC.COSINE,
initial_cap=1000, # Optional: pre-allocate space
)
HNSW - Approximate search, best for larger datasets:
vector_options = VectorFieldOptions.hnsw(
type=VectorFieldOptions.TYPE.FLOAT32,
dimension=768,
distance_metric=VectorFieldOptions.DISTANCE_METRIC.COSINE,
m=16, # Optional: max outgoing edges per node
ef_construction=200, # Optional: construction-time search width
ef_runtime=10, # Optional: query-time search width
)
Distance Metrics¶
COSINE- Cosine similarity (most common for text embeddings)L2- Euclidean distanceIP- Inner product
Vector Data Types¶
FLOAT32- 32-bit floating point (most common)FLOAT64- 64-bit floating point
For querying vector fields, see Making Queries: Vector Search.
Embedded Models (JsonModel Only)¶
JsonModel supports embedding models within other models:
from redis_om import JsonModel, Field
class Address(JsonModel):
street: str
city: str = Field(index=True)
zipcode: str
country: str = "USA"
class Meta:
embedded = True
class Customer(JsonModel, index=True):
name: str
age: int
address: Address
Embedded models:
- Must have
embedded = Truein their Meta class - Are stored as nested JSON within the parent document
- Can have their own indexed fields (included in parent's index)
- Are not separately queryable -- query through the parent model
Next Steps¶
- Learn how to query your models in Making Queries
- Configure index migrations in Migrations