Skip to main content

A drop-in replacement for `redis-om`, built out of frustration.

Project description

CodeRabbit Pull Request Reviews



Redis OM

Object mapping, and more, for Redis and Python


Version License Build Status

Redis OM Python makes it easy to model Redis data in your Python applications.

Install the package from PyPI as pyredis-om, then import aredis_om for the async API or redis_om for the generated sync mirror. This release targets Pydantic v2.

📚 The full documentation lives in docs/, also at Docs Website. This README is just the essentials.

Table of contents

💡 Why Redis OM?

Redis OM provides high-level abstractions that make it easy to model and query data in Redis with modern Python applications.

The current release includes:

  • Declarative object mapping for Redis objects
  • Declarative secondary-index generation
  • Fluent APIs for querying Redis
  • Async-first APIs with a generated sync mirror
  • Lazy Meta.database resolution, callable connection providers, runtime reassignment
  • Default model TTLs via Meta.default_ttl
  • Bulk fetches with get_many(), explicit pipeline composition
  • Redis Cluster (cluster=True or ?cluster=true in the URL)
  • Embedded JSON sorting, GEO queries, vector similarity search (FLAT/HNSW)
  • Embedded list containment queries (Workspace.users << User(name="John"))
  • Comprehensive token escaping for TAG and TEXT fields
  • GEO queries with Coordinates / GeoFilter, plus raw GEO* access — see docs/geo_queries.mdx
  • Zero-downtime schema migrations via FT.ALIASUPDATE — opt in with Meta.zero_downtime_migrations = True to swap RediSearch indexes atomically without deleting documents and with no query-gap window; rolling-deploy-safe conservative mode plus explicit allow_forward_swap=True pre-deploy step — see docs/migrations.mdx
  • AtomicCounter backed by Redis 8.8 INCREX — see docs/atomic_counter.mdx
  • RedisArray for Redis 8.8+ sparse, index-addressable arrays — see docs/redis_arrays.mdx
  • Hash field TTL (HEXPIRE / HGETEX / HGETDEL / HSETEX) on HashModel for Redis 7.4+ / 8.0+ — see docs/hash_field_ttl.mdx
  • RedisStream wrapper around the X* family with 8.2/8.4/8.6/8.8 extensions (XACKDEL, XDELEX, XNACK, IDMP, XREADGROUP ... CLAIM) — see docs/streams.mdx
  • AtomicString + MSETEX (SET IFEQ / IFNE, DELEX, DIGEST, bulk MSETEX) for Redis 8.4+ — see docs/atomic_strings.mdx
  • Vector sets (VectorSet: VADD/VSIM/VINFO/VCARD/VEMB/VLINKS/VRANDMEMBER/VREM/VSETATTR/VGETATTR) for Redis 8.8+ — see docs/vector_sets.mdx
  • Hot-keys tracker (HOTKEYS START/GET/STOP/RESET) for Redis 8.6+ — see docs/hotkeys.mdx
  • Bitmap operators (BitmapOps: BITOP DIFF/DIFF1/ANDOR/ONE) for Redis 8.2+ — see docs/bitmap_ops.mdx
  • Sorted set aggregations (SortedSetOps: ZUNION/ZINTER with AGGREGATE COUNT) for Redis 8.8+ — see docs/sorted_set_aggregations.mdx
  • Cluster admin (ClusterAdmin: CLUSTER SLOT-STATS, CLUSTER MIGRATION ...) for Redis 8.2+ cluster mode — see docs/cluster_admin.mdx
  • Keyspace notification helpers (KeyspaceEvents, build_flags, enable_keyspace_events) for Redis 2.8+ — see docs/keyspace_notifications.mdx
  • OpenTelemetry observability wrapper around redis-py 8.0 instrumentation — see docs/observability.mdx

⚡ Why execute_command?

This fork deliberately does not wrap every redis-py high-level binding (db.ft(...).search(...), db.geoadd(...), etc.). For hot paths like RediSearch, INCREX, and the AR* array commands we call db.execute_command("FT.SEARCH", ...) (or "GEOADD", "INCREX", ...) directly.

Reason What it means in practice
Faster No per-call method dispatch or argument coercion; the command name and args go straight to the socket.
More predictable Argument order matches the Redis command reference exactly. db.geoadd(... nx=True, xx=True) raised in some redis-py 5.x versions — execute_command doesn't.
Universal Works the moment Redis ships a command. INCREX (Redis 8.8+), the AR* family (8.8+ preview), and FT.AGGREGATE WITHCURSOR options all worked here before redis-py shipped typed bindings.
Cluster-safe The same call works on redis.Redis and redis.RedisCluster with no API differences.

The cost is that the caller is responsible for getting the argument order right. See docs/pipelines.mdx for tested examples.

💻 Installation

# pip
pip install pyredis-om

# uv
uv add pyredis-om

🏁 Getting started

Start Redis

docker run -p 6379:6379 redis:8-alpine

export REDIS_OM_URL="redis://localhost:6379?decode_responses=True"

The redis:8-alpine image includes the RedisJSON and RediSearch modules Redis OM needs for JSON and search features. See docs/redis_modules.mdx for other options including Redis Enterprise and OSS-only setups.

Connect

from aredis_om import get_redis_connection

redis_conn = get_redis_connection()
# Or pass an explicit URL:
redis_conn = get_redis_connection(url="redis://localhost:6379?decode_responses=True")

For Redis Cluster, see docs/cluster.mdx. For RESP2/RESP3 protocol negotiation, see docs/protocol.mdx.

Define, save, query

from redis_om import Field, HashModel, Migrator


class Customer(HashModel):
    first_name: str
    last_name: str = Field(index=True)
    age: int = Field(index=True)


Migrator().run()

andrew = Customer(first_name="Andrew", last_name="Brookins", age=38)
andrew.save()

# Reload by primary key
Customer.get(andrew.pk)

# Query — `<<` is the IN operator for TAG fields
Customer.find(Customer.last_name == "Brookins").all()
Customer.find(Customer.age >= 35).sort_by("age").page(offset=0, limit=10)

That's the whole shape. Full reference: docs/models.mdx, docs/queries.mdx.

📇 Modeling your data

Two model classes cover most needs:

from typing import Optional
from redis_om import HashModel, JsonModel, Field, EmbeddedJsonModel


class Customer(HashModel):
    first_name: str
    last_name: str = Field(index=True)
    age: int = Field(index=True)
    email: Optional[str] = Field(index=True, default=None)
  • HashModel — flat, fast, stored as a Redis hash. No List/Dict fields.
  • JsonModel — for nested structures, embedded models, List[T]/Dict[K, V].
  • EmbeddedJsonModel — a sub-document for JsonModel.address style fields.

Full details, including the lazy Meta.database, Meta.default_ttl, vector fields, and embedded List[EmbeddedJsonModel]: docs/models.mdx.

🔎 Queries, embedded models, and GEO

# Equality, range, AND/OR/NOT
Customer.find(Customer.age >= 35).all()
Customer.find(
    (Customer.last_name == "Brookins") | (Customer.first_name == "Kim")
).all()

# IN / NOT IN on TAG fields
Customer.find(Customer.last_name << ["Brookins", "Smith"]).all()
Customer.find(Customer.last_name != "Brookins").all()

# Embedded JsonModel fields
Customer.find(Customer.address.city == "San Antonio").all()

# GEO queries
from redis_om import Coordinates, GeoFilter

class Store(HashModel):
    name: str = Field(index=True)
    coordinates: Coordinates = Field(index=True)

Store.find(
    Store.coordinates == GeoFilter(
        longitude=-73.9851, latitude=40.7589, radius=2, unit="mi",
    )
).all()

Full syntax — sorting, pagination, cursors, KNN vector search, prefix matches, embedded list containment, GEO + TAG combinations: docs/queries.mdx, docs/geo_queries.mdx.

🧩 Pipelines and raw commands

Compose model queries with raw Redis commands in one round trip:

from aredis_om import HashModel, Field

class Customer(HashModel):
    first_name: str
    last_name: str = Field(index=True)


# Bulk save + atomic counter increment, in one round trip
pipe = Customer.db().pipeline(transaction=False)
pipe.incr("metrics:signups")
await Customer.add(new_customers, pipeline=pipe)
results = await pipe.execute()

Why execute_command (and not the redis-py typed bindings): see ⚡ Why execute_command? above. Full pipeline patterns — bulk fetches + secondary key lookups, GEO model + raw GEO* storage, KNN + stream publish, rate limiting + writes, cluster hash tags: docs/pipelines.mdx.

📚 Documentation

The full documentation lives in docs/. Highlights:

❤️ Contributing

See CLAUDE.md for the contributor workflow (async source of truth, make sync regeneration), and SECURITY_REVIEW.md for design notes. Open an issue on GitHub to get started.

Current local coverage baseline: 88% overall across aredis_om/ and the generated redis_om/ mirror, with 1100+ passing async + sync tests. RESP2 vs RESP3 parity is exercised end-to-end by tests/test_protocol_compat.py.

📝 License

Redis OM uses the MIT license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyredis_om-0.9.2.tar.gz (405.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyredis_om-0.9.2-py3-none-any.whl (202.6 kB view details)

Uploaded Python 3

File details

Details for the file pyredis_om-0.9.2.tar.gz.

File metadata

  • Download URL: pyredis_om-0.9.2.tar.gz
  • Upload date:
  • Size: 405.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pyredis_om-0.9.2.tar.gz
Algorithm Hash digest
SHA256 049487524f062a129c308ca699f6ddb23f6bb1c375d16f35570a616bc663e23c
MD5 3f97e00af3604c84c586229e373295d3
BLAKE2b-256 e5a775b251c9a5af4b768524e0948391a7a6ead51889a31fcb27d54b8cbe0e3b

See more details on using hashes here.

File details

Details for the file pyredis_om-0.9.2-py3-none-any.whl.

File metadata

  • Download URL: pyredis_om-0.9.2-py3-none-any.whl
  • Upload date:
  • Size: 202.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pyredis_om-0.9.2-py3-none-any.whl
Algorithm Hash digest
SHA256 60853b820a38e2d93958f06bcf1fe2aac8d79c7f79aa46d8917646341f260a98
MD5 a9fff6fc0b5169b17219150dbcaa26b2
BLAKE2b-256 75fd3bf98e6249c153aa6dcf660eedffca85605d314e89a46fc4593857edd61e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page